Clean up Config page: remove Export tab, add CreateJailDialog, fix UI details
- Remove Export tab and all its imports from ConfigPage.tsx - Remove Refresh and Reload fail2ban buttons from JailsTab; clean up associated state (reloading, reloadMsg, deactivating) and handlers - Add Create Config button to Jails tab list pane (listHeader pattern); create CreateJailDialog component that calls createJailConfigFile API - Remove Active/Inactive and 'Has local override' badges from FilterDetail and ActionDetail; remove now-unused Badge imports - Replace read-only log path spans with editable Input fields in JailConfigDetail - Export CreateJailDialog from components/config/index.ts - Mark all 5 tasks done in Docs/Tasks.md
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Field,
|
||||
Input,
|
||||
@@ -121,19 +120,6 @@ function ActionDetail({
|
||||
size="small"
|
||||
/>
|
||||
</Field>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<Badge
|
||||
appearance={action.active ? "filled" : "outline"}
|
||||
color={action.active ? "success" : "informative"}
|
||||
>
|
||||
{action.active ? "Active" : "Inactive"}
|
||||
</Badge>
|
||||
{action.has_local_override && (
|
||||
<Badge appearance="tint" color="warning" size="small">
|
||||
Has local override
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Structured editor */}
|
||||
|
||||
164
frontend/src/components/config/CreateJailDialog.tsx
Normal file
164
frontend/src/components/config/CreateJailDialog.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* CreateJailDialog — dialog for creating a new jail configuration file.
|
||||
*
|
||||
* Asks for a config file name and calls ``POST /api/config/jail-files`` on
|
||||
* confirmation, seeding the file with a minimal comment header.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogSurface,
|
||||
DialogTitle,
|
||||
Field,
|
||||
Input,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
Spinner,
|
||||
Text,
|
||||
tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import { createJailConfigFile } from "../../api/config";
|
||||
import type { ConfFileCreateRequest } from "../../types/config";
|
||||
import { ApiError } from "../../api/client";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CreateJailDialogProps {
|
||||
/** Whether the dialog is currently open. */
|
||||
open: boolean;
|
||||
/** Called when the dialog should close without taking action. */
|
||||
onClose: () => void;
|
||||
/** Called after the jail config file has been successfully created. */
|
||||
onCreated: () => void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Dialog for creating a new jail configuration file at
|
||||
* ``jail.d/{name}.conf``.
|
||||
*
|
||||
* The name field accepts a plain base name (e.g. ``my-custom-jail``); the
|
||||
* backend appends the ``.conf`` extension.
|
||||
*
|
||||
* @param props - Component props.
|
||||
* @returns JSX element.
|
||||
*/
|
||||
export function CreateJailDialog({
|
||||
open,
|
||||
onClose,
|
||||
onCreated,
|
||||
}: CreateJailDialogProps): React.JSX.Element {
|
||||
const [name, setName] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Reset form when the dialog opens.
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setName("");
|
||||
setError(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
if (submitting) return;
|
||||
onClose();
|
||||
}, [submitting, onClose]);
|
||||
|
||||
const handleConfirm = useCallback((): void => {
|
||||
const trimmedName = name.trim();
|
||||
if (!trimmedName || submitting) return;
|
||||
|
||||
const req: ConfFileCreateRequest = {
|
||||
name: trimmedName,
|
||||
content: `# ${trimmedName}\n`,
|
||||
};
|
||||
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
createJailConfigFile(req)
|
||||
.then(() => {
|
||||
onCreated();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setError(
|
||||
err instanceof ApiError ? err.message : "Failed to create jail config.",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
}, [name, submitting, onCreated]);
|
||||
|
||||
const canConfirm = name.trim() !== "" && !submitting;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(_e, data) => { if (!data.open) handleClose(); }}>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>Create Config</DialogTitle>
|
||||
<DialogContent>
|
||||
<Text
|
||||
as="p"
|
||||
size={300}
|
||||
style={{ marginBottom: tokens.spacingVerticalM }}
|
||||
>
|
||||
Creates a new jail configuration file at{" "}
|
||||
<code>jail.d/<name>.conf</code>.
|
||||
</Text>
|
||||
|
||||
{error !== null && (
|
||||
<MessageBar
|
||||
intent="error"
|
||||
style={{ marginBottom: tokens.spacingVerticalS }}
|
||||
>
|
||||
<MessageBarBody>{error}</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label="Config name"
|
||||
required
|
||||
hint='Base name without extension, e.g. "my-custom-jail". Must not already exist.'
|
||||
>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(_e, d) => { setName(d.value); }}
|
||||
placeholder="my-custom-jail"
|
||||
disabled={submitting}
|
||||
/>
|
||||
</Field>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
appearance="secondary"
|
||||
onClick={handleClose}
|
||||
disabled={submitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
appearance="primary"
|
||||
onClick={handleConfirm}
|
||||
disabled={!canConfirm}
|
||||
icon={submitting ? <Spinner size="extra-small" /> : undefined}
|
||||
>
|
||||
{submitting ? "Creating…" : "Create Config"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Field,
|
||||
Input,
|
||||
@@ -95,19 +94,6 @@ function FilterDetail({
|
||||
size="small"
|
||||
/>
|
||||
</Field>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<Badge
|
||||
appearance={filter.active ? "filled" : "outline"}
|
||||
color={filter.active ? "success" : "informative"}
|
||||
>
|
||||
{filter.active ? "Active" : "Inactive"}
|
||||
</Badge>
|
||||
{filter.has_local_override && (
|
||||
<Badge appearance="tint" color="warning" size="small">
|
||||
Has local override
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Structured editor */}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
ArrowClockwise24Regular,
|
||||
Add24Regular,
|
||||
Dismiss24Regular,
|
||||
LockClosed24Regular,
|
||||
LockOpen24Regular,
|
||||
@@ -50,6 +50,7 @@ import { useJailConfigs } from "../../hooks/useConfig";
|
||||
import { ActivateJailDialog } from "./ActivateJailDialog";
|
||||
import { AutoSaveIndicator } from "./AutoSaveIndicator";
|
||||
import { ConfigListDetail } from "./ConfigListDetail";
|
||||
import { CreateJailDialog } from "./CreateJailDialog";
|
||||
import { RawConfigSection } from "./RawConfigSection";
|
||||
import { RegexList } from "./RegexList";
|
||||
import { useConfigStyles } from "./configStyles";
|
||||
@@ -295,11 +296,16 @@ function JailConfigDetail({
|
||||
(none)
|
||||
</Text>
|
||||
) : (
|
||||
logPaths.map((p) => (
|
||||
<div key={p} className={styles.regexItem}>
|
||||
<span className={styles.codeFont} style={{ flexGrow: 1 }}>
|
||||
{p}
|
||||
</span>
|
||||
logPaths.map((p, i) => (
|
||||
<div key={i} className={styles.regexItem}>
|
||||
<Input
|
||||
className={styles.codeFont}
|
||||
style={{ flexGrow: 1 }}
|
||||
value={p}
|
||||
onChange={(_e, d) => {
|
||||
setLogPaths((prev) => prev.map((v, j) => (j === i ? d.value : v)));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
appearance="subtle"
|
||||
icon={<Dismiss24Regular />}
|
||||
@@ -583,18 +589,16 @@ function InactiveJailDetail({
|
||||
*/
|
||||
export function JailsTab(): React.JSX.Element {
|
||||
const styles = useConfigStyles();
|
||||
const { jails, loading, error, refresh, updateJail, reloadAll } =
|
||||
const { jails, loading, error, refresh, updateJail } =
|
||||
useJailConfigs();
|
||||
const { activeJails } = useConfigActiveStatus();
|
||||
const [selectedName, setSelectedName] = useState<string | null>(null);
|
||||
const [reloading, setReloading] = useState(false);
|
||||
const [reloadMsg, setReloadMsg] = useState<string | null>(null);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
|
||||
// Inactive jails
|
||||
const [inactiveJails, setInactiveJails] = useState<InactiveJail[]>([]);
|
||||
const [inactiveLoading, setInactiveLoading] = useState(false);
|
||||
const [activateTarget, setActivateTarget] = useState<InactiveJail | null>(null);
|
||||
const [deactivating, setDeactivating] = useState(false);
|
||||
|
||||
const loadInactive = useCallback((): void => {
|
||||
setInactiveLoading(true);
|
||||
@@ -608,42 +612,14 @@ export function JailsTab(): React.JSX.Element {
|
||||
loadInactive();
|
||||
}, [loadInactive]);
|
||||
|
||||
const handleRefresh = useCallback((): void => {
|
||||
refresh();
|
||||
loadInactive();
|
||||
}, [refresh, loadInactive]);
|
||||
|
||||
const handleReload = useCallback(async () => {
|
||||
setReloading(true);
|
||||
setReloadMsg(null);
|
||||
try {
|
||||
await reloadAll();
|
||||
setReloadMsg("fail2ban reloaded.");
|
||||
refresh();
|
||||
loadInactive();
|
||||
} catch (err: unknown) {
|
||||
setReloadMsg(err instanceof ApiError ? err.message : "Reload failed.");
|
||||
} finally {
|
||||
setReloading(false);
|
||||
}
|
||||
}, [reloadAll, refresh, loadInactive]);
|
||||
|
||||
const handleDeactivate = useCallback((name: string): void => {
|
||||
setDeactivating(true);
|
||||
setReloadMsg(null);
|
||||
deactivateJail(name)
|
||||
.then(() => {
|
||||
setReloadMsg(`Jail "${name}" deactivated.`);
|
||||
setSelectedName(null);
|
||||
refresh();
|
||||
loadInactive();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setReloadMsg(
|
||||
err instanceof ApiError ? err.message : "Deactivation failed."
|
||||
);
|
||||
})
|
||||
.finally(() => { setDeactivating(false); });
|
||||
.catch(() => { /* non-critical — list refreshes on next load */ });
|
||||
}, [refresh, loadInactive]);
|
||||
|
||||
const handleActivated = useCallback((): void => {
|
||||
@@ -717,34 +693,19 @@ export function JailsTab(): React.JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const listHeader = (
|
||||
<Button
|
||||
appearance="outline"
|
||||
icon={<Add24Regular />}
|
||||
size="small"
|
||||
onClick={() => { setCreateDialogOpen(true); }}
|
||||
>
|
||||
Create Config
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.tabContent}>
|
||||
<div className={styles.buttonRow}>
|
||||
<Button
|
||||
appearance="secondary"
|
||||
icon={<ArrowClockwise24Regular />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button
|
||||
appearance="secondary"
|
||||
icon={<ArrowClockwise24Regular />}
|
||||
disabled={reloading || deactivating}
|
||||
onClick={() => void handleReload()}
|
||||
>
|
||||
{reloading ? "Reloading…" : "Reload fail2ban"}
|
||||
</Button>
|
||||
</div>
|
||||
{reloadMsg && (
|
||||
<MessageBar
|
||||
style={{ marginTop: tokens.spacingVerticalS }}
|
||||
intent="info"
|
||||
>
|
||||
<MessageBarBody>{reloadMsg}</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: tokens.spacingVerticalM }}>
|
||||
<ConfigListDetail
|
||||
items={listItems}
|
||||
@@ -753,6 +714,7 @@ export function JailsTab(): React.JSX.Element {
|
||||
onSelect={setSelectedName}
|
||||
loading={false}
|
||||
error={null}
|
||||
listHeader={listHeader}
|
||||
>
|
||||
{selectedActiveJail !== undefined ? (
|
||||
<JailConfigDetail
|
||||
@@ -775,6 +737,16 @@ export function JailsTab(): React.JSX.Element {
|
||||
onClose={() => { setActivateTarget(null); }}
|
||||
onActivated={handleActivated}
|
||||
/>
|
||||
|
||||
<CreateJailDialog
|
||||
open={createDialogOpen}
|
||||
onClose={() => { setCreateDialogOpen(false); }}
|
||||
onCreated={() => {
|
||||
setCreateDialogOpen(false);
|
||||
refresh();
|
||||
loadInactive();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ export { CreateActionDialog } from "./CreateActionDialog";
|
||||
export type { CreateActionDialogProps } from "./CreateActionDialog";
|
||||
export { CreateFilterDialog } from "./CreateFilterDialog";
|
||||
export type { CreateFilterDialogProps } from "./CreateFilterDialog";
|
||||
export { CreateJailDialog } from "./CreateJailDialog";
|
||||
export type { CreateJailDialogProps } from "./CreateJailDialog";
|
||||
export { ExportTab } from "./ExportTab";
|
||||
export { FilterForm } from "./FilterForm";
|
||||
export type { FilterFormProps } from "./FilterForm";
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Tab, TabList, Text, makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { DocumentArrowDown24Regular } from "@fluentui/react-icons";
|
||||
import {
|
||||
ActionsTab,
|
||||
ExportTab,
|
||||
FiltersTab,
|
||||
GlobalTab,
|
||||
JailsTab,
|
||||
@@ -62,8 +60,7 @@ type TabValue =
|
||||
| "global"
|
||||
| "server"
|
||||
| "map"
|
||||
| "regex"
|
||||
| "export";
|
||||
| "regex";
|
||||
|
||||
export function ConfigPage(): React.JSX.Element {
|
||||
const styles = useStyles();
|
||||
@@ -94,7 +91,6 @@ export function ConfigPage(): React.JSX.Element {
|
||||
<Tab value="server">Server</Tab>
|
||||
<Tab value="map">Map</Tab>
|
||||
<Tab value="regex">Regex Tester</Tab>
|
||||
<Tab value="export" icon={<DocumentArrowDown24Regular />}>Export</Tab>
|
||||
</TabList>
|
||||
|
||||
<div className={styles.tabContent} key={tab}>
|
||||
@@ -105,7 +101,6 @@ export function ConfigPage(): React.JSX.Element {
|
||||
{tab === "server" && <ServerTab />}
|
||||
{tab === "map" && <MapTab />}
|
||||
{tab === "regex" && <RegexTesterTab />}
|
||||
{tab === "export" && <ExportTab />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user