- Create DashboardFilterBar component with time-range and origin-filter toggle-button groups in a single card row (Stage 7, Tasks 7.1–7.3) - Integrate filter bar below ServerStatusBar in DashboardPage; remove filter toolbars from the Ban List section header (Task 7.2) - Add 6 tests covering rendering, active-state reflection, and callbacks - tsc --noEmit, eslint, npm run build, npm test all pass (27/27 tests)
196 lines
7.0 KiB
TypeScript
196 lines
7.0 KiB
TypeScript
/**
|
|
* Dashboard page.
|
|
*
|
|
* Composes the fail2ban server status bar at the top, a shared time-range
|
|
* selector, and the ban list showing aggregate bans from the fail2ban
|
|
* database. The time-range selection controls how far back to look.
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import { Text, makeStyles, tokens } from "@fluentui/react-components";
|
|
import { BanTable } from "../components/BanTable";
|
|
import { BanTrendChart } from "../components/BanTrendChart";
|
|
import { ChartStateWrapper } from "../components/ChartStateWrapper";
|
|
import { DashboardFilterBar } from "../components/DashboardFilterBar";
|
|
import { JailDistributionChart } from "../components/JailDistributionChart";
|
|
import { ServerStatusBar } from "../components/ServerStatusBar";
|
|
import { TopCountriesBarChart } from "../components/TopCountriesBarChart";
|
|
import { TopCountriesPieChart } from "../components/TopCountriesPieChart";
|
|
import { useDashboardCountryData } from "../hooks/useDashboardCountryData";
|
|
import type { BanOriginFilter, TimeRange } from "../types/ban";
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Styles
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const useStyles = makeStyles({
|
|
root: {
|
|
display: "flex",
|
|
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,
|
|
},
|
|
chartsRow: {
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
gap: tokens.spacingHorizontalL,
|
|
flexWrap: "wrap",
|
|
},
|
|
chartCard: {
|
|
flex: "1 1 300px",
|
|
minWidth: "280px",
|
|
},
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Component
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Main dashboard landing page.
|
|
*
|
|
* Displays the fail2ban server status, a time-range selector, and the
|
|
* ban list table.
|
|
*/
|
|
export function DashboardPage(): React.JSX.Element {
|
|
const styles = useStyles();
|
|
const [timeRange, setTimeRange] = useState<TimeRange>("24h");
|
|
const [originFilter, setOriginFilter] = useState<BanOriginFilter>("all");
|
|
|
|
const { countries, countryNames, isLoading: countryLoading, error: countryError, reload: reloadCountry } =
|
|
useDashboardCountryData(timeRange, originFilter);
|
|
|
|
return (
|
|
<div className={styles.root}>
|
|
{/* ------------------------------------------------------------------ */}
|
|
{/* Server status bar */}
|
|
{/* ------------------------------------------------------------------ */}
|
|
<ServerStatusBar />
|
|
|
|
{/* ------------------------------------------------------------------ */}
|
|
{/* Global filter bar */}
|
|
{/* ------------------------------------------------------------------ */}
|
|
<DashboardFilterBar
|
|
timeRange={timeRange}
|
|
onTimeRangeChange={setTimeRange}
|
|
originFilter={originFilter}
|
|
onOriginFilterChange={setOriginFilter}
|
|
/>
|
|
|
|
{/* ------------------------------------------------------------------ */}
|
|
{/* Ban Trend section */}
|
|
{/* ------------------------------------------------------------------ */}
|
|
<div className={styles.section}>
|
|
<div className={styles.sectionHeader}>
|
|
<Text as="h2" size={500} weight="semibold">
|
|
Ban Trend
|
|
</Text>
|
|
</div>
|
|
<div className={styles.tabContent}>
|
|
<BanTrendChart timeRange={timeRange} origin={originFilter} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* ------------------------------------------------------------------ */}
|
|
{/* Charts section */}
|
|
{/* ------------------------------------------------------------------ */}
|
|
<div className={styles.section}>
|
|
<div className={styles.sectionHeader}>
|
|
<Text as="h2" size={500} weight="semibold">
|
|
Top Countries
|
|
</Text>
|
|
</div>
|
|
<div className={styles.tabContent}>
|
|
<ChartStateWrapper
|
|
isLoading={countryLoading}
|
|
error={countryError}
|
|
onRetry={reloadCountry}
|
|
isEmpty={!countryLoading && Object.keys(countries).length === 0}
|
|
emptyMessage="No ban data for the selected period."
|
|
>
|
|
<div className={styles.chartsRow}>
|
|
<div className={styles.chartCard}>
|
|
<TopCountriesPieChart
|
|
countries={countries}
|
|
countryNames={countryNames}
|
|
/>
|
|
</div>
|
|
<div className={styles.chartCard}>
|
|
<TopCountriesBarChart
|
|
countries={countries}
|
|
countryNames={countryNames}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</ChartStateWrapper>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ------------------------------------------------------------------ */}
|
|
{/* Jail Distribution section */}
|
|
{/* ------------------------------------------------------------------ */}
|
|
<div className={styles.section}>
|
|
<div className={styles.sectionHeader}>
|
|
<Text as="h2" size={500} weight="semibold">
|
|
Jail Distribution
|
|
</Text>
|
|
</div>
|
|
<div className={styles.tabContent}>
|
|
<JailDistributionChart timeRange={timeRange} origin={originFilter} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* ------------------------------------------------------------------ */}
|
|
{/* Ban list section */}
|
|
{/* ------------------------------------------------------------------ */}
|
|
<div className={styles.section}>
|
|
<div className={styles.sectionHeader}>
|
|
<Text as="h2" size={500} weight="semibold">
|
|
Ban List
|
|
</Text>
|
|
</div>
|
|
|
|
{/* Ban table */}
|
|
<div className={styles.tabContent}>
|
|
<BanTable timeRange={timeRange} origin={originFilter} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|