feat: Stage 4 — fail2ban connection and server status
This commit is contained in:
179
frontend/src/components/ServerStatusBar.tsx
Normal file
179
frontend/src/components/ServerStatusBar.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* `ServerStatusBar` component.
|
||||
*
|
||||
* Displays a persistent bar at the top of the dashboard showing the
|
||||
* fail2ban server health snapshot: connectivity status, version, active
|
||||
* jail count, and aggregated ban/failure totals.
|
||||
*
|
||||
* Polls `GET /api/dashboard/status` every 30 seconds and on window focus
|
||||
* via the {@link useServerStatus} hook.
|
||||
*/
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
makeStyles,
|
||||
Spinner,
|
||||
Text,
|
||||
tokens,
|
||||
Tooltip,
|
||||
} from "@fluentui/react-components";
|
||||
import { ArrowClockwiseRegular, ShieldRegular } from "@fluentui/react-icons";
|
||||
import { useServerStatus } from "../hooks/useServerStatus";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const useStyles = makeStyles({
|
||||
bar: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingHorizontalL,
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalL}`,
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
borderRadius: tokens.borderRadiusMedium,
|
||||
borderTopWidth: "1px",
|
||||
borderTopStyle: "solid",
|
||||
borderTopColor: tokens.colorNeutralStroke2,
|
||||
borderRightWidth: "1px",
|
||||
borderRightStyle: "solid",
|
||||
borderRightColor: tokens.colorNeutralStroke2,
|
||||
borderBottomWidth: "1px",
|
||||
borderBottomStyle: "solid",
|
||||
borderBottomColor: tokens.colorNeutralStroke2,
|
||||
borderLeftWidth: "1px",
|
||||
borderLeftStyle: "solid",
|
||||
borderLeftColor: tokens.colorNeutralStroke2,
|
||||
marginBottom: tokens.spacingVerticalL,
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
statusGroup: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingHorizontalS,
|
||||
},
|
||||
statGroup: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingHorizontalXS,
|
||||
},
|
||||
statValue: {
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
fontWeight: 600,
|
||||
},
|
||||
spacer: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
errorText: {
|
||||
color: tokens.colorPaletteRedForeground1,
|
||||
fontSize: "12px",
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Persistent bar displaying fail2ban server health.
|
||||
*
|
||||
* Render this at the top of the dashboard page (and any page that should
|
||||
* show live server status).
|
||||
*/
|
||||
export function ServerStatusBar(): JSX.Element {
|
||||
const styles = useStyles();
|
||||
const { status, loading, error, refresh } = useServerStatus();
|
||||
|
||||
return (
|
||||
<div className={styles.bar} role="status" aria-label="fail2ban server status">
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
{/* Online / Offline badge */}
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
<div className={styles.statusGroup}>
|
||||
<ShieldRegular fontSize={16} />
|
||||
{loading && !status ? (
|
||||
<Spinner size="extra-tiny" label="Checking…" labelPosition="after" />
|
||||
) : (
|
||||
<Badge
|
||||
appearance="filled"
|
||||
color={status?.online ? "success" : "danger"}
|
||||
aria-label={status?.online ? "fail2ban online" : "fail2ban offline"}
|
||||
>
|
||||
{status?.online ? "Online" : "Offline"}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
{/* Version */}
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
{status?.version != null && (
|
||||
<Tooltip content="fail2ban version" relationship="description">
|
||||
<Text size={200} className={styles.statValue}>
|
||||
v{status.version}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
{/* Stats (only when online) */}
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
{status?.online === true && (
|
||||
<>
|
||||
<Tooltip content="Active jails" relationship="description">
|
||||
<div className={styles.statGroup}>
|
||||
<Text size={200}>Jails:</Text>
|
||||
<Text size={200} className={styles.statValue}>
|
||||
{status.active_jails}
|
||||
</Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Currently banned IPs" relationship="description">
|
||||
<div className={styles.statGroup}>
|
||||
<Text size={200}>Bans:</Text>
|
||||
<Text size={200} className={styles.statValue}>
|
||||
{status.total_bans}
|
||||
</Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Currently failing IPs" relationship="description">
|
||||
<div className={styles.statGroup}>
|
||||
<Text size={200}>Failures:</Text>
|
||||
<Text size={200} className={styles.statValue}>
|
||||
{status.total_failures}
|
||||
</Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
{/* Error message */}
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
{error != null && (
|
||||
<Text className={styles.errorText} aria-live="polite">
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<div className={styles.spacer} />
|
||||
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
{/* Refresh button */}
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
<Tooltip content="Refresh server status" relationship="label">
|
||||
<Button
|
||||
appearance="subtle"
|
||||
size="small"
|
||||
icon={loading ? <Spinner size="extra-tiny" /> : <ArrowClockwiseRegular />}
|
||||
onClick={refresh}
|
||||
aria-label="Refresh server status"
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user