200 lines
7.0 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
|
|
|