Files
BanGUI/frontend/src/pages/DashboardPage.tsx
2026-05-04 13:13:01 +02:00

200 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.
*
* Sections are wrapped with SectionErrorBoundary to provide graceful
* degradation if individual charts or tables fail to render.
*/
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 { ServerStatusBar } from "../components/ServerStatusBar";
import { TopCountriesBarChart } from "../components/TopCountriesBarChart";
import { TopCountriesPieChart } from "../components/TopCountriesPieChart";
import { SectionErrorBoundary } from "../components/SectionErrorBoundary";
import { useCommonSectionStyles } from "../components/commonStyles";
import { useDashboardCountryData } from "../hooks/useDashboardCountryData";
import { DashboardFilterProvider, useDashboardFilters } from "./DashboardFilterProvider";
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
const useStyles = makeStyles({
root: {
display: "flex",
flexDirection: "column",
gap: tokens.spacingVerticalM,
},
filterRow: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalM,
flexWrap: "wrap",
},
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. Each section is protected with a SectionErrorBoundary
* so that a failure in one section does not crash the entire page.
*/
function DashboardPageContent(): React.JSX.Element {
const styles = useStyles();
const { timeRange, originFilter, source, setTimeRange, setOriginFilter } = useDashboardFilters();
const { countries, countryNames, loading: countryLoading, error: countryError, reload: reloadCountry } =
useDashboardCountryData(timeRange, originFilter, source);
const sectionStyles = useCommonSectionStyles();
return (
<div className={styles.root} data-testid="dashboard">
{/* ------------------------------------------------------------------ */}
{/* Server status bar */}
{/* ------------------------------------------------------------------ */}
<SectionErrorBoundary sectionName="Server Status">
<ServerStatusBar />
</SectionErrorBoundary>
{/* ------------------------------------------------------------------ */}
{/* Global filter bar */}
{/* ------------------------------------------------------------------ */}
<div className={styles.filterRow}>
<DashboardFilterBar
timeRange={timeRange}
onTimeRangeChange={setTimeRange}
originFilter={originFilter}
onOriginFilterChange={setOriginFilter}
/>
</div>
{/* ------------------------------------------------------------------ */}
{/* Ban Trend section */}
{/* ------------------------------------------------------------------ */}
<div className={sectionStyles.section}>
<div className={styles.sectionHeader}>
<Text as="h2" size={500} weight="semibold">
Ban Trend
</Text>
</div>
<div className={styles.tabContent}>
<SectionErrorBoundary sectionName="Ban Trend Chart">
<BanTrendChart
timeRange={timeRange}
origin={originFilter}
source={source}
/>
</SectionErrorBoundary>
</div>
</div>
{/* ------------------------------------------------------------------ */}
{/* Charts section */}
{/* ------------------------------------------------------------------ */}
<div className={sectionStyles.section}>
<div className={styles.sectionHeader}>
<Text as="h2" size={500} weight="semibold">
Top Countries
</Text>
</div>
<div className={styles.tabContent}>
<SectionErrorBoundary sectionName="Country Charts">
<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>
</SectionErrorBoundary>
</div>
</div>
{/* ------------------------------------------------------------------ */}
{/* Ban list section */}
{/* ------------------------------------------------------------------ */}
<div className={sectionStyles.section}>
<div className={styles.sectionHeader}>
<Text as="h2" size={500} weight="semibold">
Ban List
</Text>
</div>
{/* Ban table */}
<div className={styles.tabContent}>
<SectionErrorBoundary sectionName="Ban List">
<BanTable
timeRange={timeRange}
origin={originFilter}
source={source}
/>
</SectionErrorBoundary>
</div>
</div>
</div>
);
}
export function DashboardPage(): React.JSX.Element {
return (
<DashboardFilterProvider>
<DashboardPageContent />
</DashboardFilterProvider>
);
}