Stage 11: polish, cross-cutting concerns & hardening
- 11.1 MainLayout health indicator: warning MessageBar when fail2ban offline - 11.2 formatDate utility + TimezoneProvider + GET /api/setup/timezone - 11.3 Responsive sidebar: auto-collapse <640px, media query listener - 11.4 PageFeedback (PageLoading/PageError/PageEmpty), BanTable updated - 11.5 prefers-reduced-motion: disable sidebar transition - 11.6 WorldMap ARIA: role/tabIndex/aria-label/onKeyDown for countries - 11.7 Health transition logging (fail2ban_came_online/went_offline) - 11.8 Global handlers: Fail2BanConnectionError/ProtocolError -> 502 - 11.9 379 tests pass, 82% coverage, ruff+mypy+tsc+eslint clean - Timezone endpoint: setup_service.get_timezone, 5 new tests
This commit is contained in:
@@ -6,11 +6,14 @@
|
||||
* icon-only (48 px) on small screens.
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
makeStyles,
|
||||
mergeClasses,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
MessageBarTitle,
|
||||
Text,
|
||||
tokens,
|
||||
Tooltip,
|
||||
@@ -27,6 +30,7 @@ import {
|
||||
} from "@fluentui/react-icons";
|
||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../providers/AuthProvider";
|
||||
import { useServerStatus } from "../hooks/useServerStatus";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styles
|
||||
@@ -56,6 +60,10 @@ const useStyles = makeStyles({
|
||||
transition: "width 200ms ease",
|
||||
overflow: "hidden",
|
||||
flexShrink: 0,
|
||||
// Honour the OS/browser reduced-motion preference.
|
||||
"@media (prefers-reduced-motion: reduce)": {
|
||||
transition: "none",
|
||||
},
|
||||
},
|
||||
sidebarCollapsed: {
|
||||
width: SIDEBAR_COLLAPSED,
|
||||
@@ -137,6 +145,13 @@ const useStyles = makeStyles({
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
},
|
||||
warningBar: {
|
||||
flexShrink: 0,
|
||||
paddingLeft: tokens.spacingHorizontalM,
|
||||
paddingRight: tokens.spacingHorizontalM,
|
||||
paddingTop: tokens.spacingVerticalXS,
|
||||
paddingBottom: tokens.spacingVerticalXS,
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
maxWidth: "1440px",
|
||||
@@ -180,7 +195,23 @@ export function MainLayout(): React.JSX.Element {
|
||||
const styles = useStyles();
|
||||
const { logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
// Initialise collapsed based on screen width so narrow viewports start
|
||||
// with the icon-only sidebar rather than the full-width one.
|
||||
const [collapsed, setCollapsed] = useState(() => window.innerWidth < 640);
|
||||
const { status } = useServerStatus();
|
||||
|
||||
/** True only after the first successful poll and fail2ban is unreachable. */
|
||||
const serverOffline = status !== null && !status.online;
|
||||
|
||||
// Auto-collapse / auto-expand when the viewport crosses the 640 px breakpoint.
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia("(max-width: 639px)");
|
||||
const handler = (e: MediaQueryListEvent): void => {
|
||||
setCollapsed(e.matches);
|
||||
};
|
||||
mq.addEventListener("change", handler);
|
||||
return (): void => { mq.removeEventListener("change", handler); };
|
||||
}, []);
|
||||
|
||||
const toggleCollapse = useCallback(() => {
|
||||
setCollapsed((prev) => !prev);
|
||||
@@ -270,6 +301,18 @@ export function MainLayout(): React.JSX.Element {
|
||||
{/* Main content */}
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
<main className={styles.main}>
|
||||
{/* Connection health warning — shown when fail2ban is unreachable */}
|
||||
{serverOffline && (
|
||||
<div className={styles.warningBar} role="alert">
|
||||
<MessageBar intent="warning">
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>fail2ban Unreachable</MessageBarTitle>
|
||||
The connection to the fail2ban server has been lost. Some
|
||||
features may be temporarily unavailable.
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user