refactor: move jail detail sub-sections from pages/jail to components/jail
Move reusable UI section components (JailInfoSection, PatternsSection, BantimeEscalationSection, IgnoreListSection, CodeList) from pages/jail/ to components/jail/, aligning with the project convention that pages/ contains only route-level entry points while components/ contains reusable UI building blocks. Changes: - Move 5 section components + jailDetailPageStyles.ts to components/jail/ - Update import paths in moved components (relative paths to commonStyles) - Update JailDetailPage.tsx imports to reference components/jail/ - Delete empty pages/jail/ directory - Document pages/ vs components/ distinction in Web-Development.md All components use standard import structure and TypeScript passes type checking. BannedIpsSection was already correctly placed in components/jail/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
61
frontend/src/components/jail/BantimeEscalationSection.tsx
Normal file
61
frontend/src/components/jail/BantimeEscalationSection.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Badge, Text } from "@fluentui/react-components";
|
||||
import { useCommonSectionStyles } from "../commonStyles";
|
||||
import { useJailDetailPageStyles } from "./jailDetailPageStyles";
|
||||
import type { Jail } from "../../types/jail";
|
||||
import { formatSeconds } from "../../utils/formatDate";
|
||||
|
||||
interface BantimeEscalationSectionProps {
|
||||
jail: Jail;
|
||||
}
|
||||
|
||||
export function BantimeEscalationSection({ jail }: BantimeEscalationSectionProps): React.JSX.Element | null {
|
||||
const styles = useJailDetailPageStyles();
|
||||
const sectionStyles = useCommonSectionStyles();
|
||||
const esc = jail.bantime_escalation;
|
||||
if (!esc?.increment) return null;
|
||||
|
||||
return (
|
||||
<div className={sectionStyles.section}>
|
||||
<div className={sectionStyles.sectionHeader}>
|
||||
<Text as="h2" size={500} weight="semibold">
|
||||
Ban-time Escalation
|
||||
</Text>
|
||||
<Badge appearance="filled" color="informative">enabled</Badge>
|
||||
</div>
|
||||
<div className={styles.grid}>
|
||||
{esc.factor !== null && (
|
||||
<>
|
||||
<Text className={styles.label}>Factor:</Text>
|
||||
<Text className={styles.mono}>{String(esc.factor)}</Text>
|
||||
</>
|
||||
)}
|
||||
{esc.formula && (
|
||||
<>
|
||||
<Text className={styles.label}>Formula:</Text>
|
||||
<Text className={styles.mono}>{esc.formula}</Text>
|
||||
</>
|
||||
)}
|
||||
{esc.multipliers && (
|
||||
<>
|
||||
<Text className={styles.label}>Multipliers:</Text>
|
||||
<Text className={styles.mono}>{esc.multipliers}</Text>
|
||||
</>
|
||||
)}
|
||||
{esc.max_time !== null && (
|
||||
<>
|
||||
<Text className={styles.label}>Max time:</Text>
|
||||
<Text>{formatSeconds(esc.max_time)}</Text>
|
||||
</>
|
||||
)}
|
||||
{esc.rnd_time !== null && (
|
||||
<>
|
||||
<Text className={styles.label}>Random jitter:</Text>
|
||||
<Text>{formatSeconds(esc.rnd_time)}</Text>
|
||||
</>
|
||||
)}
|
||||
<Text className={styles.label}>Count across all jails:</Text>
|
||||
<Text>{esc.overall_jails ? "yes" : "no"}</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
frontend/src/components/jail/CodeList.tsx
Normal file
24
frontend/src/components/jail/CodeList.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Text } from "@fluentui/react-components";
|
||||
import { useJailDetailPageStyles } from "./jailDetailPageStyles";
|
||||
|
||||
interface CodeListProps {
|
||||
items: string[];
|
||||
empty: string;
|
||||
}
|
||||
|
||||
export function CodeList({ items, empty }: CodeListProps): React.JSX.Element {
|
||||
const styles = useJailDetailPageStyles();
|
||||
if (items.length === 0) {
|
||||
return <Text size={200} style={{ color: "var(--colorNeutralForeground3)" }}>{empty}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.codeList}>
|
||||
{items.map((item, i) => (
|
||||
<span key={`${item}-${String(i)}`} className={styles.codeItem}>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
137
frontend/src/components/jail/IgnoreListSection.tsx
Normal file
137
frontend/src/components/jail/IgnoreListSection.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Field,
|
||||
Input,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
Switch,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@fluentui/react-components";
|
||||
import { DismissRegular } from "@fluentui/react-icons";
|
||||
import { useCommonSectionStyles } from "../commonStyles";
|
||||
import { useJailDetailPageStyles } from "./jailDetailPageStyles";
|
||||
|
||||
interface IgnoreListSectionProps {
|
||||
jailName: string;
|
||||
ignoreList: string[];
|
||||
ignoreSelf: boolean;
|
||||
onAdd: (ip: string) => Promise<void>;
|
||||
onRemove: (ip: string) => Promise<void>;
|
||||
onToggleIgnoreSelf: (on: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export function IgnoreListSection({
|
||||
jailName: _jailName,
|
||||
ignoreList,
|
||||
ignoreSelf,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onToggleIgnoreSelf,
|
||||
}: IgnoreListSectionProps): React.JSX.Element {
|
||||
const styles = useJailDetailPageStyles();
|
||||
const sectionStyles = useCommonSectionStyles();
|
||||
const [inputVal, setInputVal] = useState("");
|
||||
const [opError, setOpError] = useState<string | null>(null);
|
||||
|
||||
const handleAdd = (): void => {
|
||||
if (!inputVal.trim()) return;
|
||||
setOpError(null);
|
||||
onAdd(inputVal.trim())
|
||||
.then(() => {
|
||||
setInputVal("");
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setOpError(err instanceof Error ? err.message : String(err));
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemove = (ip: string): void => {
|
||||
setOpError(null);
|
||||
onRemove(ip).catch((err: unknown) => {
|
||||
setOpError(err instanceof Error ? err.message : String(err));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={sectionStyles.section}>
|
||||
<div className={sectionStyles.sectionHeader}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "var(--spacingHorizontalM)" }}>
|
||||
<Text as="h2" size={500} weight="semibold">
|
||||
Ignore List (IP Whitelist)
|
||||
</Text>
|
||||
</div>
|
||||
<Badge appearance="tint">{String(ignoreList.length)}</Badge>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
label="Ignore self — exclude this server's own IP addresses from banning"
|
||||
checked={ignoreSelf}
|
||||
onChange={(_e, data): void => {
|
||||
onToggleIgnoreSelf(data.checked).catch((err: unknown) => {
|
||||
setOpError(err instanceof Error ? err.message : String(err));
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{opError && (
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>{opError}</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<Field label="Add IP or CIDR network">
|
||||
<Input
|
||||
placeholder="e.g. 10.0.0.0/8 or 192.168.1.1"
|
||||
value={inputVal}
|
||||
onChange={(_, d) => {
|
||||
setInputVal(d.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleAdd();
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Button
|
||||
appearance="primary"
|
||||
onClick={handleAdd}
|
||||
disabled={!inputVal.trim()}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{ignoreList.length === 0 ? (
|
||||
<Text size={200} style={{ color: "var(--colorNeutralForeground3)" }}>
|
||||
The ignore list is empty.
|
||||
</Text>
|
||||
) : (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
||||
{ignoreList.map((ip) => (
|
||||
<div key={ip} className={styles.ignoreRow}>
|
||||
<Text className={styles.mono}>{ip}</Text>
|
||||
<Tooltip content={`Remove ${ip} from ignore list`} relationship="label">
|
||||
<Button
|
||||
size="small"
|
||||
appearance="subtle"
|
||||
icon={<DismissRegular />}
|
||||
onClick={() => {
|
||||
handleRemove(ip);
|
||||
}}
|
||||
aria-label={`Remove ${ip}`}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
146
frontend/src/components/jail/JailInfoSection.tsx
Normal file
146
frontend/src/components/jail/JailInfoSection.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
Text,
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
ArrowClockwiseRegular,
|
||||
ArrowSyncRegular,
|
||||
PauseRegular,
|
||||
PlayRegular,
|
||||
StopRegular,
|
||||
} from "@fluentui/react-icons";
|
||||
import { useCommonSectionStyles } from "../commonStyles";
|
||||
import { useJailDetailPageStyles } from "./jailDetailPageStyles";
|
||||
import type { Jail } from "../../types/jail";
|
||||
|
||||
interface JailInfoProps {
|
||||
jail: Jail;
|
||||
onRefresh: () => void;
|
||||
onStart: () => Promise<void>;
|
||||
onStop: () => Promise<void>;
|
||||
onSetIdle: (on: boolean) => Promise<void>;
|
||||
onReload: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function JailInfoSection({ jail, onRefresh, onStart, onStop, onSetIdle, onReload }: JailInfoProps): React.JSX.Element {
|
||||
const styles = useJailDetailPageStyles();
|
||||
const sectionStyles = useCommonSectionStyles();
|
||||
const navigate = useNavigate();
|
||||
const [ctrlError, setCtrlError] = useState<string | null>(null);
|
||||
|
||||
const handle =
|
||||
(fn: () => Promise<unknown>, postNavigate = false) =>
|
||||
(): void => {
|
||||
setCtrlError(null);
|
||||
fn()
|
||||
.then(() => {
|
||||
if (postNavigate) {
|
||||
navigate("/jails");
|
||||
} else {
|
||||
onRefresh();
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setCtrlError(msg);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={sectionStyles.section}>
|
||||
<div className={sectionStyles.sectionHeader}>
|
||||
<div className={styles.headerRow}>
|
||||
<Text size={600} weight="semibold" style={{ fontFamily: "Consolas, 'Courier New', monospace" }}>
|
||||
{jail.name}
|
||||
</Text>
|
||||
{jail.running ? (
|
||||
jail.idle ? (
|
||||
<Badge appearance="filled" color="warning">idle</Badge>
|
||||
) : (
|
||||
<Badge appearance="filled" color="success">running</Badge>
|
||||
)
|
||||
) : (
|
||||
<Badge appearance="filled" color="danger">stopped</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
appearance="subtle"
|
||||
icon={<ArrowClockwiseRegular />}
|
||||
onClick={onRefresh}
|
||||
aria-label="Refresh"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ctrlError && (
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>{ctrlError}</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
<div className={styles.controlRow}>
|
||||
{jail.running ? (
|
||||
<Button appearance="secondary" icon={<StopRegular />} onClick={handle(onStop)}>
|
||||
Stop
|
||||
</Button>
|
||||
) : (
|
||||
<Button appearance="primary" icon={<PlayRegular />} onClick={handle(onStart)}>
|
||||
Start
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
appearance="outline"
|
||||
icon={<PauseRegular />}
|
||||
onClick={handle(() => onSetIdle(!jail.idle))}
|
||||
disabled={!jail.running}
|
||||
>
|
||||
{jail.idle ? "Resume" : "Set Idle"}
|
||||
</Button>
|
||||
<Button appearance="outline" icon={<ArrowSyncRegular />} onClick={handle(onReload)}>
|
||||
Reload
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{jail.status && (
|
||||
<div className={styles.grid} style={{ marginTop: "var(--spacingVerticalS)" }}>
|
||||
<Text className={styles.label}>Currently banned:</Text>
|
||||
<Text>{String(jail.status.currently_banned)}</Text>
|
||||
<Text className={styles.label}>Total banned:</Text>
|
||||
<Text>{String(jail.status.total_banned)}</Text>
|
||||
<Text className={styles.label}>Currently failed:</Text>
|
||||
<Text>{String(jail.status.currently_failed)}</Text>
|
||||
<Text className={styles.label}>Total failed:</Text>
|
||||
<Text>{String(jail.status.total_failed)}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.grid} style={{ marginTop: "var(--spacingVerticalS)" }}>
|
||||
<Text className={styles.label}>Backend:</Text>
|
||||
<Text className={styles.mono}>{jail.backend}</Text>
|
||||
<Text className={styles.label}>Find time:</Text>
|
||||
<Text>{String(jail.find_time)}</Text>
|
||||
<Text className={styles.label}>Ban time:</Text>
|
||||
<Text>{String(jail.ban_time)}</Text>
|
||||
<Text className={styles.label}>Max retry:</Text>
|
||||
<Text>{String(jail.max_retry)}</Text>
|
||||
{jail.date_pattern && (
|
||||
<>
|
||||
<Text className={styles.label}>Date pattern:</Text>
|
||||
<Text className={styles.mono}>{jail.date_pattern}</Text>
|
||||
</>
|
||||
)}
|
||||
{jail.log_encoding && (
|
||||
<>
|
||||
<Text className={styles.label}>Log encoding:</Text>
|
||||
<Text className={styles.mono}>{jail.log_encoding}</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
frontend/src/components/jail/PatternsSection.tsx
Normal file
53
frontend/src/components/jail/PatternsSection.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Text, makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { useCommonSectionStyles } from "../commonStyles";
|
||||
import type { Jail } from "../../types/jail";
|
||||
import { CodeList } from "./CodeList";
|
||||
|
||||
interface PatternsSectionProps {
|
||||
jail: Jail;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
spacedHeading: {
|
||||
marginTop: tokens.spacingVerticalS,
|
||||
},
|
||||
});
|
||||
|
||||
export function PatternsSection({ jail }: PatternsSectionProps): React.JSX.Element {
|
||||
const sectionStyles = useCommonSectionStyles();
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<div className={sectionStyles.section}>
|
||||
<div className={sectionStyles.sectionHeader}>
|
||||
<Text as="h2" size={500} weight="semibold">
|
||||
Log Paths & Patterns
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text size={300} weight="semibold">
|
||||
Log Paths
|
||||
</Text>
|
||||
<CodeList items={jail.log_paths} empty="No log paths configured." />
|
||||
|
||||
<Text size={300} weight="semibold" className={styles.spacedHeading}>
|
||||
Fail Regex
|
||||
</Text>
|
||||
<CodeList items={jail.fail_regex} empty="No fail-regex patterns." />
|
||||
|
||||
<Text size={300} weight="semibold" className={styles.spacedHeading}>
|
||||
Ignore Regex
|
||||
</Text>
|
||||
<CodeList items={jail.ignore_regex} empty="No ignore-regex patterns." />
|
||||
|
||||
{jail.actions.length > 0 && (
|
||||
<>
|
||||
<Text size={300} weight="semibold" className={styles.spacedHeading}>
|
||||
Actions
|
||||
</Text>
|
||||
<CodeList items={jail.actions} empty="" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
frontend/src/components/jail/jailDetailPageStyles.ts
Normal file
75
frontend/src/components/jail/jailDetailPageStyles.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useJailDetailPageStyles = makeStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalL,
|
||||
},
|
||||
breadcrumb: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingHorizontalS,
|
||||
},
|
||||
headerRow: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingHorizontalM,
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
controlRow: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: tokens.spacingHorizontalS,
|
||||
},
|
||||
grid: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "max-content 1fr",
|
||||
gap: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalM}`,
|
||||
alignItems: "baseline",
|
||||
},
|
||||
label: {
|
||||
fontWeight: tokens.fontWeightSemibold,
|
||||
color: tokens.colorNeutralForeground2,
|
||||
},
|
||||
mono: {
|
||||
fontFamily: tokens.fontFamilyMonospace,
|
||||
fontSize: tokens.fontSizeBase200,
|
||||
},
|
||||
codeList: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalXXS,
|
||||
paddingTop: tokens.spacingVerticalXS,
|
||||
},
|
||||
codeItem: {
|
||||
fontFamily: tokens.fontFamilyMonospace,
|
||||
fontSize: tokens.fontSizeBase200,
|
||||
padding: `2px ${tokens.spacingHorizontalS}`,
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
borderRadius: tokens.borderRadiusSmall,
|
||||
wordBreak: "break-all",
|
||||
},
|
||||
centred: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: tokens.spacingVerticalXXL,
|
||||
},
|
||||
ignoreRow: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: tokens.spacingHorizontalS,
|
||||
padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
borderRadius: tokens.borderRadiusSmall,
|
||||
},
|
||||
formRow: {
|
||||
display: "flex",
|
||||
gap: tokens.spacingHorizontalM,
|
||||
alignItems: "flex-end",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
formField: { minWidth: "200px", flexGrow: 1 },
|
||||
});
|
||||
Reference in New Issue
Block a user