feat: implement dashboard ban overview (Stage 5)
- Add ban_service reading fail2ban SQLite DB via read-only aiosqlite - Add geo_service resolving IPs via ip-api.com with 10k in-memory cache - Add GET /api/dashboard/bans and GET /api/dashboard/accesses endpoints - Add TimeRange, DashboardBanItem, DashboardBanListResponse, AccessListItem, AccessListResponse models in models/ban.py - Build BanTable component (Fluent UI DataGrid) with bans/accesses modes, pagination, loading/error/empty states, and ban-count badges - Build useBans hook managing time-range and pagination state - Update DashboardPage: status bar + time-range toolbar + tab switcher - Add 37 new backend tests (ban service, geo service, dashboard router) - All 141 tests pass; ruff/mypy --strict/tsc --noEmit clean
This commit is contained in:
@@ -1,12 +1,31 @@
|
||||
/**
|
||||
* Dashboard page.
|
||||
*
|
||||
* Shows the fail2ban server status bar at the top.
|
||||
* Full ban-list implementation is delivered in Stage 5.
|
||||
* Composes the fail2ban server status bar at the top, a shared time-range
|
||||
* selector, and two tabs: "Ban List" (aggregate bans) and "Access List"
|
||||
* (individual matched log lines). The time-range selection is shared
|
||||
* between both tabs so users can compare data for the same period.
|
||||
*/
|
||||
|
||||
import { Text, makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Tab,
|
||||
TabList,
|
||||
Text,
|
||||
ToggleButton,
|
||||
Toolbar,
|
||||
makeStyles,
|
||||
tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import { BanTable } from "../components/BanTable";
|
||||
import { ServerStatusBar } from "../components/ServerStatusBar";
|
||||
import type { TimeRange } from "../types/ban";
|
||||
import { TIME_RANGE_LABELS } from "../types/ban";
|
||||
import type { BanTableMode } from "../hooks/useBans";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
@@ -14,22 +33,116 @@ const useStyles = makeStyles({
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalM,
|
||||
},
|
||||
section: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalS,
|
||||
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,
|
||||
padding: tokens.spacingVerticalM,
|
||||
},
|
||||
sectionHeader: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
gap: tokens.spacingHorizontalM,
|
||||
paddingBottom: tokens.spacingVerticalS,
|
||||
borderBottomWidth: "1px",
|
||||
borderBottomStyle: "solid",
|
||||
borderBottomColor: tokens.colorNeutralStroke2,
|
||||
},
|
||||
tabContent: {
|
||||
paddingTop: tokens.spacingVerticalS,
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Ordered time-range presets for the toolbar. */
|
||||
const TIME_RANGES: TimeRange[] = ["24h", "7d", "30d", "365d"];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Dashboard page — renders the server status bar and a Stage 5 placeholder.
|
||||
* Main dashboard landing page.
|
||||
*
|
||||
* Displays the fail2ban server status, a time-range selector, and a
|
||||
* tabbed view toggling between the ban list and the access list.
|
||||
*/
|
||||
export function DashboardPage(): JSX.Element {
|
||||
export function DashboardPage(): React.JSX.Element {
|
||||
const styles = useStyles();
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>("24h");
|
||||
const [activeTab, setActiveTab] = useState<BanTableMode>("bans");
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
{/* Server status bar */}
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
<ServerStatusBar />
|
||||
<Text as="h1" size={700} weight="semibold">
|
||||
Dashboard
|
||||
</Text>
|
||||
<Text as="p" size={300}>
|
||||
Ban overview will be implemented in Stage 5.
|
||||
</Text>
|
||||
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
{/* Ban / access list section */}
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<Text as="h2" size={500} weight="semibold">
|
||||
{activeTab === "bans" ? "Ban List" : "Access List"}
|
||||
</Text>
|
||||
|
||||
{/* Shared time-range selector */}
|
||||
<Toolbar aria-label="Time range" size="small">
|
||||
{TIME_RANGES.map((r) => (
|
||||
<ToggleButton
|
||||
key={r}
|
||||
size="small"
|
||||
checked={timeRange === r}
|
||||
onClick={() => {
|
||||
setTimeRange(r);
|
||||
}}
|
||||
aria-pressed={timeRange === r}
|
||||
>
|
||||
{TIME_RANGE_LABELS[r]}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</Toolbar>
|
||||
</div>
|
||||
|
||||
{/* Tab switcher */}
|
||||
<TabList
|
||||
selectedValue={activeTab}
|
||||
onTabSelect={(_, data) => {
|
||||
setActiveTab(data.value as BanTableMode);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<Tab value="bans">Ban List</Tab>
|
||||
<Tab value="accesses">Access List</Tab>
|
||||
</TabList>
|
||||
|
||||
{/* Active tab content */}
|
||||
<div className={styles.tabContent}>
|
||||
<BanTable mode={activeTab} timeRange={timeRange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user