Finish Task 13: extract remaining page subcomponents and clean page files
This commit is contained in:
@@ -16,33 +16,22 @@ import {
|
||||
DataGridHeader,
|
||||
DataGridHeaderCell,
|
||||
DataGridRow,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
Spinner,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellLayout,
|
||||
TableColumnDefinition,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Text,
|
||||
Toolbar,
|
||||
ToolbarButton,
|
||||
TableColumnDefinition,
|
||||
createTableColumn,
|
||||
makeStyles,
|
||||
tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import { useCardStyles } from "../theme/commonStyles";
|
||||
import {
|
||||
ArrowCounterclockwiseRegular,
|
||||
ArrowLeftRegular,
|
||||
ChevronLeftRegular,
|
||||
ChevronRightRegular,
|
||||
} from "@fluentui/react-icons";
|
||||
import { DashboardFilterBar } from "../components/DashboardFilterBar";
|
||||
import { useHistory, useIpHistory } from "../hooks/useHistory";
|
||||
import { useHistory } from "../hooks/useHistory";
|
||||
import { IpDetailView } from "./history/IpDetailView";
|
||||
import type { HistoryBanItem, HistoryQuery, TimeRange } from "../types/history";
|
||||
import type { BanOriginFilter } from "../types/ban";
|
||||
|
||||
@@ -216,169 +205,6 @@ const HISTORY_COLUMNS = (
|
||||
}),
|
||||
] as ReturnType<typeof createTableColumn<HistoryBanItem>>[];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IpDetailView — per-IP detail view
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface IpDetailViewProps {
|
||||
ip: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
function IpDetailView({ ip, onBack }: IpDetailViewProps): React.JSX.Element {
|
||||
const styles = useStyles();
|
||||
const cardStyles = useCardStyles();
|
||||
const { detail, loading, error, refresh } = useIpHistory(ip);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "center", padding: tokens.spacingVerticalXL }}
|
||||
>
|
||||
<Spinner label={`Loading history for ${ip}…`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>{error}</MessageBarBody>
|
||||
</MessageBar>
|
||||
);
|
||||
}
|
||||
|
||||
if (!detail) {
|
||||
return (
|
||||
<MessageBar intent="warning">
|
||||
<MessageBarBody>No history found for {ip}.</MessageBarBody>
|
||||
</MessageBar>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: tokens.spacingVerticalL }}>
|
||||
{/* Back button + heading */}
|
||||
<div className={styles.header}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: tokens.spacingHorizontalM }}>
|
||||
<Button
|
||||
icon={<ArrowLeftRegular />}
|
||||
appearance="subtle"
|
||||
onClick={onBack}
|
||||
>
|
||||
Back to list
|
||||
</Button>
|
||||
<Text as="h2" size={600} weight="semibold" className={styles.monoText}>
|
||||
{ip}
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
icon={<ArrowCounterclockwiseRegular />}
|
||||
appearance="subtle"
|
||||
onClick={(): void => {
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Summary grid */}
|
||||
<div className={`${cardStyles.card} ${styles.detailGrid}`}>
|
||||
<div className={styles.detailField}>
|
||||
<span className={styles.detailLabel}>Total Bans</span>
|
||||
<span className={styles.detailValue}>{String(detail.total_bans)}</span>
|
||||
</div>
|
||||
<div className={styles.detailField}>
|
||||
<span className={styles.detailLabel}>Total Failures</span>
|
||||
<span className={styles.detailValue}>{String(detail.total_failures)}</span>
|
||||
</div>
|
||||
<div className={styles.detailField}>
|
||||
<span className={styles.detailLabel}>Last Banned</span>
|
||||
<span className={styles.detailValue}>
|
||||
{detail.last_ban_at
|
||||
? new Date(detail.last_ban_at).toLocaleString()
|
||||
: "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.detailField}>
|
||||
<span className={styles.detailLabel}>Country</span>
|
||||
<span className={styles.detailValue}>
|
||||
{detail.country_name ?? detail.country_code ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.detailField}>
|
||||
<span className={styles.detailLabel}>ASN</span>
|
||||
<span className={styles.detailValue}>{detail.asn ?? "—"}</span>
|
||||
</div>
|
||||
<div className={styles.detailField}>
|
||||
<span className={styles.detailLabel}>Organisation</span>
|
||||
<span className={styles.detailValue}>{detail.org ?? "—"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline table */}
|
||||
<Text weight="semibold" size={400}>
|
||||
Ban Timeline ({String(detail.timeline.length)} events)
|
||||
</Text>
|
||||
|
||||
<div className={styles.tableWrapper}>
|
||||
<Table size="small" aria-label="Ban timeline">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Banned At</TableHeaderCell>
|
||||
<TableHeaderCell>Jail</TableHeaderCell>
|
||||
<TableHeaderCell>Failures</TableHeaderCell>
|
||||
<TableHeaderCell>Times Banned</TableHeaderCell>
|
||||
<TableHeaderCell>Matched Lines</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{detail.timeline.map((event) => (
|
||||
<TableRow key={`${event.jail}-${event.banned_at}`}>
|
||||
<TableCell>
|
||||
<TableCellLayout>
|
||||
{new Date(event.banned_at).toLocaleString()}
|
||||
</TableCellLayout>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCellLayout>{event.jail}</TableCellLayout>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCellLayout>{String(event.failures)}</TableCellLayout>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCellLayout>{String(event.ban_count)}</TableCellLayout>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCellLayout>
|
||||
{event.matches.length === 0 ? (
|
||||
<Text size={200} style={{ color: tokens.colorNeutralForeground3 }}>
|
||||
—
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
size={100}
|
||||
style={{
|
||||
fontFamily: "Consolas, 'Courier New', monospace",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
>
|
||||
{event.matches.join("\n")}
|
||||
</Text>
|
||||
)}
|
||||
</TableCellLayout>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HistoryPage — main component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user