Add origin field and filter for ban sources (Tasks 1 & 2)
- Task 1: Mark imported blocklist IP addresses
- Add BanOrigin type and _derive_origin() to ban.py model
- Populate origin field in ban_service list_bans() and bans_by_country()
- BanTable and MapPage companion table show origin badge column
- Tests: origin derivation in test_ban_service.py and test_dashboard.py
- Task 2: Add origin filter to dashboard and world map
- ban_service: _origin_sql_filter() helper; origin param on list_bans()
and bans_by_country()
- dashboard router: optional origin query param forwarded to service
- Frontend: BanOriginFilter type + BAN_ORIGIN_FILTER_LABELS in ban.ts
- fetchBans / fetchBansByCountry forward origin to API
- useBans / useMapData accept and pass origin; page resets on change
- BanTable accepts origin prop; DashboardPage adds segmented filter
- MapPage adds origin Select next to time-range picker
- Tests: origin filter assertions in test_ban_service and test_dashboard
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
* bans when no country is selected).
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
@@ -29,7 +30,10 @@ import {
|
||||
import { ArrowCounterclockwiseRegular, DismissRegular } from "@fluentui/react-icons";
|
||||
import { WorldMap } from "../components/WorldMap";
|
||||
import { useMapData } from "../hooks/useMapData";
|
||||
import { fetchMapColorThresholds } from "../api/config";
|
||||
import type { TimeRange } from "../types/map";
|
||||
import type { BanOriginFilter } from "../types/ban";
|
||||
import { BAN_ORIGIN_FILTER_LABELS } from "../types/ban";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styles
|
||||
@@ -86,10 +90,30 @@ const TIME_RANGE_OPTIONS: { label: string; value: TimeRange }[] = [
|
||||
export function MapPage(): React.JSX.Element {
|
||||
const styles = useStyles();
|
||||
const [range, setRange] = useState<TimeRange>("24h");
|
||||
const [originFilter, setOriginFilter] = useState<BanOriginFilter>("all");
|
||||
const [selectedCountry, setSelectedCountry] = useState<string | null>(null);
|
||||
const [thresholdLow, setThresholdLow] = useState<number>(20);
|
||||
const [thresholdMedium, setThresholdMedium] = useState<number>(50);
|
||||
const [thresholdHigh, setThresholdHigh] = useState<number>(100);
|
||||
|
||||
const { countries, countryNames, bans, total, loading, error, refresh } =
|
||||
useMapData(range);
|
||||
useMapData(range, originFilter);
|
||||
|
||||
// Fetch color thresholds on mount
|
||||
useEffect(() => {
|
||||
const loadThresholds = async (): Promise<void> => {
|
||||
try {
|
||||
const thresholds = await fetchMapColorThresholds();
|
||||
setThresholdLow(thresholds.threshold_low);
|
||||
setThresholdMedium(thresholds.threshold_medium);
|
||||
setThresholdHigh(thresholds.threshold_high);
|
||||
} catch (err) {
|
||||
// Silently fall back to defaults if fetch fails
|
||||
console.warn("Failed to load map color thresholds:", err);
|
||||
}
|
||||
};
|
||||
void loadThresholds();
|
||||
}, []);
|
||||
|
||||
/** Bans visible in the companion table (filtered by selected country). */
|
||||
const visibleBans = useMemo(() => {
|
||||
@@ -128,6 +152,23 @@ export function MapPage(): React.JSX.Element {
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{/* Origin filter */}
|
||||
<Select
|
||||
aria-label="Origin filter"
|
||||
value={originFilter}
|
||||
onChange={(_ev, data): void => {
|
||||
setOriginFilter(data.value as BanOriginFilter);
|
||||
setSelectedCountry(null);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
{(["all", "blocklist", "selfblock"] as BanOriginFilter[]).map((f) => (
|
||||
<option key={f} value={f}>
|
||||
{BAN_ORIGIN_FILTER_LABELS[f]}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<ToolbarButton
|
||||
icon={<ArrowCounterclockwiseRegular />}
|
||||
onClick={(): void => {
|
||||
@@ -162,6 +203,9 @@ export function MapPage(): React.JSX.Element {
|
||||
countries={countries}
|
||||
selectedCountry={selectedCountry}
|
||||
onSelectCountry={setSelectedCountry}
|
||||
thresholdLow={thresholdLow}
|
||||
thresholdMedium={thresholdMedium}
|
||||
thresholdHigh={thresholdHigh}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -211,13 +255,14 @@ export function MapPage(): React.JSX.Element {
|
||||
<TableHeaderCell>Jail</TableHeaderCell>
|
||||
<TableHeaderCell>Banned At</TableHeaderCell>
|
||||
<TableHeaderCell>Country</TableHeaderCell>
|
||||
<TableHeaderCell>Origin</TableHeaderCell>
|
||||
<TableHeaderCell>Times Banned</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{visibleBans.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5}>
|
||||
<TableCell colSpan={6}>
|
||||
<TableCellLayout>
|
||||
<Text size={200} style={{ color: tokens.colorNeutralForeground3 }}>
|
||||
No bans found.
|
||||
@@ -244,6 +289,16 @@ export function MapPage(): React.JSX.Element {
|
||||
{ban.country_name ?? ban.country_code ?? "—"}
|
||||
</TableCellLayout>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCellLayout>
|
||||
<Badge
|
||||
appearance="tint"
|
||||
color={ban.origin === "blocklist" ? "brand" : "informative"}
|
||||
>
|
||||
{ban.origin === "blocklist" ? "Blocklist" : "Selfblock"}
|
||||
</Badge>
|
||||
</TableCellLayout>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCellLayout>{String(ban.ban_count)}</TableCellLayout>
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user