Refactor frontend pages and config components into single-component files for Task 13

This commit is contained in:
2026-04-19 09:30:35 +02:00
parent 6c053cdaee
commit 38b9d35255
31 changed files with 2158 additions and 2603 deletions

View File

@@ -0,0 +1,218 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { formatSeconds } from "../../utils/formatDate";
import {
Badge,
Button,
DataGrid,
DataGridBody,
DataGridCell,
DataGridHeader,
DataGridHeaderCell,
DataGridRow,
MessageBar,
MessageBarBody,
Spinner,
Text,
Tooltip,
} from "@fluentui/react-components";
import {
ArrowClockwiseRegular,
ArrowSyncRegular,
PauseRegular,
PlayRegular,
StopRegular,
} from "@fluentui/react-icons";
import { useCommonSectionStyles } from "../../theme/commonStyles";
import { useJailsPageStyles } from "./jailsPageStyles";
import { useJails } from "../../hooks/useJails";
import type { JailSummary } from "../../types/jail";
const jailColumns = [
{
columnId: "name",
renderHeaderCell: () => "Jail",
renderCell: (j: JailSummary) => (
<Link to={`/jails/${encodeURIComponent(j.name)}`} style={{ textDecoration: "none" }}>
<Text style={{ fontFamily: "Consolas, 'Courier New', monospace", fontSize: "0.85rem" }}>
{j.name}
</Text>
</Link>
),
compare: (a: JailSummary, b: JailSummary) => a.name.localeCompare(b.name),
},
{
columnId: "status",
renderHeaderCell: () => "Status",
renderCell: (j: JailSummary) => {
if (!j.running) return <Badge appearance="filled" color="danger">stopped</Badge>;
if (j.idle) return <Badge appearance="filled" color="warning">idle</Badge>;
return <Badge appearance="filled" color="success">running</Badge>;
},
compare: (a: JailSummary, b: JailSummary) => {
if (a.running !== b.running) return a.running ? -1 : 1;
if (a.idle !== b.idle) return a.idle ? 1 : -1;
return 0;
},
},
{
columnId: "backend",
renderHeaderCell: () => "Backend",
renderCell: (j: JailSummary) => <Text size={200}>{j.backend}</Text>,
compare: (a: JailSummary, b: JailSummary) => a.backend.localeCompare(b.backend),
},
{
columnId: "banned",
renderHeaderCell: () => "Banned",
renderCell: (j: JailSummary) => (
<Text size={200}>{j.status ? String(j.status.currently_banned) : "—"}</Text>
),
compare: (a: JailSummary, b: JailSummary) => (a.status?.currently_banned ?? 0) - (b.status?.currently_banned ?? 0),
},
{
columnId: "failed",
renderHeaderCell: () => "Failed",
renderCell: (j: JailSummary) => (
<Text size={200}>{j.status ? String(j.status.currently_failed) : "—"}</Text>
),
compare: (a: JailSummary, b: JailSummary) => (a.status?.currently_failed ?? 0) - (b.status?.currently_failed ?? 0),
},
{
columnId: "findTime",
renderHeaderCell: () => "Find Time",
renderCell: (j: JailSummary) => <Text size={200}>{formatSeconds(j.find_time)}</Text>,
compare: (a: JailSummary, b: JailSummary) => a.find_time - b.find_time,
},
{
columnId: "banTime",
renderHeaderCell: () => "Ban Time",
renderCell: (j: JailSummary) => <Text size={200}>{formatSeconds(j.ban_time)}</Text>,
compare: (a: JailSummary, b: JailSummary) => a.ban_time - b.ban_time,
},
{
columnId: "maxRetry",
renderHeaderCell: () => "Max Retry",
renderCell: (j: JailSummary) => <Text size={200}>{String(j.max_retry)}</Text>,
compare: (a: JailSummary, b: JailSummary) => a.max_retry - b.max_retry,
},
];
export function JailOverviewSection(): React.JSX.Element {
const styles = useJailsPageStyles();
const sectionStyles = useCommonSectionStyles();
const { jails, total, loading, error, refresh, startJail, stopJail, setIdle, reloadJail, reloadAll } = useJails();
const [opError, setOpError] = useState<string | null>(null);
const handle = (fn: () => Promise<void>): void => {
setOpError(null);
fn().catch((err: unknown) => {
setOpError(err instanceof Error ? err.message : String(err));
});
};
return (
<div className={sectionStyles.section}>
<div className={sectionStyles.sectionHeader}>
<Text as="h2" size={500} weight="semibold">
Jail Overview
{total > 0 && (
<Badge appearance="tint" style={{ marginLeft: "8px" }}>
{String(total)}
</Badge>
)}
</Text>
<div className={styles.actionRow}>
<Button
size="small"
appearance="subtle"
icon={<ArrowSyncRegular />}
onClick={() => { handle(reloadAll); }}
>
Reload All
</Button>
<Button size="small" appearance="subtle" icon={<ArrowClockwiseRegular />} onClick={refresh}>
Refresh
</Button>
</div>
</div>
{opError && (
<MessageBar intent="error">
<MessageBarBody>{opError}</MessageBarBody>
</MessageBar>
)}
{error && (
<MessageBar intent="error">
<MessageBarBody>Failed to load jails: {error}</MessageBarBody>
</MessageBar>
)}
{loading && jails.length === 0 ? (
<div className={styles.centred}>
<Spinner label="Loading jails…" />
</div>
) : (
<div className={styles.tableWrapper}>
<DataGrid items={jails} columns={jailColumns} getRowId={(j: JailSummary) => j.name} focusMode="composite">
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell }) => <DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>}
</DataGridRow>
</DataGridHeader>
<DataGridBody<JailSummary>>
{({ item }) => (
<DataGridRow<JailSummary> key={item.name}>
{({ renderCell, columnId }) => {
if (columnId === "status") {
return (
<DataGridCell>
<div style={{ display: "flex", gap: "6px", alignItems: "center" }}>
{renderCell(item)}
<Tooltip content={item.running ? "Stop jail" : "Start jail"} relationship="label">
<Button
size="small"
appearance="subtle"
icon={item.running ? <StopRegular /> : <PlayRegular />}
onClick={() => {
handle(async () => {
if (item.running) await stopJail(item.name);
else await startJail(item.name);
});
}}
aria-label={item.running ? `Stop ${item.name}` : `Start ${item.name}`}
/>
</Tooltip>
<Tooltip content={item.idle ? "Resume from idle" : "Set idle"} relationship="label">
<Button
size="small"
appearance="subtle"
icon={<PauseRegular />}
onClick={() => { handle(async () => setIdle(item.name, !item.idle)); }}
disabled={!item.running}
aria-label={`Toggle idle for ${item.name}`}
/>
</Tooltip>
<Tooltip content="Reload jail" relationship="label">
<Button
size="small"
appearance="subtle"
icon={<ArrowSyncRegular />}
onClick={() => { handle(async () => reloadJail(item.name)); }}
aria-label={`Reload ${item.name}`}
/>
</Tooltip>
</div>
</DataGridCell>
);
}
return <DataGridCell>{renderCell(item)}</DataGridCell>;
}}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>
</div>
)}
</div>
);
}