fix(config): gate useJails() calls behind dialog open prop
Refactored AssignActionDialog and AssignFilterDialog to only render dialog content when open=true. This prevents useJails() from being called when dialogs are closed, eliminating unnecessary GET /api/jails requests. Implementation uses inner components (AssignActionDialogInner, AssignFilterDialogInner) that are only mounted when the dialog is open. The Dialog wrapper remains in the outer component to preserve Fluent UI animation behavior. Fixed test setup for AssignFilterDialog to properly call assignFilterToJail from the mocked onAssign callback. Fixes TASK-BUG-08. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -54,9 +54,8 @@ export interface AssignActionDialogProps {
|
||||
/**
|
||||
* Confirmation dialog for assigning an action to a jail.
|
||||
*
|
||||
* Fetches running jails from the API for the jail selector. The user can
|
||||
* optionally request a fail2ban reload immediately after the assignment is
|
||||
* written.
|
||||
* Wraps AssignActionDialogInner and only renders the inner component when
|
||||
* the dialog is open, preventing unnecessary API calls when closed.
|
||||
*
|
||||
* @param props - Component props.
|
||||
* @returns JSX element.
|
||||
@@ -68,6 +67,43 @@ export function AssignActionDialog({
|
||||
onAssigned,
|
||||
onAssign,
|
||||
}: AssignActionDialogProps): React.JSX.Element {
|
||||
const handleClose = useCallback((): void => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(_e, data) => { if (!data.open) handleClose(); }}>
|
||||
<DialogSurface>
|
||||
{open && (
|
||||
<AssignActionDialogInner
|
||||
actionName={actionName}
|
||||
onClose={handleClose}
|
||||
onAssigned={onAssigned}
|
||||
onAssign={onAssign}
|
||||
/>
|
||||
)}
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
interface AssignActionDialogInnerProps {
|
||||
actionName: string | null;
|
||||
onClose: () => void;
|
||||
onAssigned: () => void;
|
||||
onAssign: (jailName: string, payload: AssignActionRequest, reload: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner component that contains the dialog body and only renders when open.
|
||||
* This ensures useJails() is only called when the dialog is actually visible.
|
||||
*/
|
||||
function AssignActionDialogInner({
|
||||
actionName,
|
||||
onClose,
|
||||
onAssigned,
|
||||
onAssign,
|
||||
}: AssignActionDialogInnerProps): React.JSX.Element {
|
||||
const { jails, loading: jailsLoading, error: jailsError } = useJails();
|
||||
const [selectedJail, setSelectedJail] = useState("");
|
||||
const [reload, setReload] = useState(false);
|
||||
@@ -77,17 +113,16 @@ export function AssignActionDialog({
|
||||
const activeJails = jails.filter((j) => j.enabled);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setError(null);
|
||||
setSelectedJail("");
|
||||
setReload(false);
|
||||
}, [open]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (jailsError && open) {
|
||||
if (jailsError) {
|
||||
setError(jailsError);
|
||||
}
|
||||
}, [jailsError, open]);
|
||||
}, [jailsError]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
if (submitting) return;
|
||||
@@ -119,86 +154,82 @@ export function AssignActionDialog({
|
||||
const canConfirm = selectedJail !== "" && !submitting && !jailsLoading;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(_e, data) => { if (!data.open) handleClose(); }}>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>Assign Action to Jail</DialogTitle>
|
||||
<DialogContent>
|
||||
{actionName !== null && (
|
||||
<Text
|
||||
as="p"
|
||||
size={300}
|
||||
style={{ marginBottom: tokens.spacingVerticalM }}
|
||||
>
|
||||
Assign action{" "}
|
||||
<Text weight="semibold" style={{ fontFamily: "monospace" }}>
|
||||
{actionName}
|
||||
</Text>{" "}
|
||||
to a jail. This appends the action to the jail's{" "}
|
||||
<code>action</code> list in its <code>.local</code> override file.
|
||||
</Text>
|
||||
)}
|
||||
<DialogBody>
|
||||
<DialogTitle>Assign Action to Jail</DialogTitle>
|
||||
<DialogContent>
|
||||
{actionName !== null && (
|
||||
<Text
|
||||
as="p"
|
||||
size={300}
|
||||
style={{ marginBottom: tokens.spacingVerticalM }}
|
||||
>
|
||||
Assign action{" "}
|
||||
<Text weight="semibold" style={{ fontFamily: "monospace" }}>
|
||||
{actionName}
|
||||
</Text>{" "}
|
||||
to a jail. This appends the action to the jail's{" "}
|
||||
<code>action</code> list in its <code>.local</code> override file.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{error !== null && (
|
||||
<MessageBar
|
||||
intent="error"
|
||||
style={{ marginBottom: tokens.spacingVerticalS }}
|
||||
>
|
||||
<MessageBarBody>{error}</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
{error !== null && (
|
||||
<MessageBar
|
||||
intent="error"
|
||||
style={{ marginBottom: tokens.spacingVerticalS }}
|
||||
>
|
||||
<MessageBarBody>{error}</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label="Target jail"
|
||||
required
|
||||
hint="Only currently enabled jails are listed."
|
||||
<Field
|
||||
label="Target jail"
|
||||
required
|
||||
hint="Only currently enabled jails are listed."
|
||||
>
|
||||
{jailsLoading ? (
|
||||
<Spinner size="extra-small" label="Loading jails…" />
|
||||
) : (
|
||||
<Select
|
||||
value={selectedJail}
|
||||
onChange={(_e, d) => { setSelectedJail(d.value); }}
|
||||
aria-label="Target jail"
|
||||
>
|
||||
{jailsLoading ? (
|
||||
<Spinner size="extra-small" label="Loading jails…" />
|
||||
) : (
|
||||
<Select
|
||||
value={selectedJail}
|
||||
onChange={(_e, d) => { setSelectedJail(d.value); }}
|
||||
aria-label="Target jail"
|
||||
>
|
||||
<option value="" disabled>
|
||||
— select a jail —
|
||||
</option>
|
||||
{activeJails.map((j) => (
|
||||
<option key={j.name} value={j.name}>
|
||||
{j.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</Field>
|
||||
<option value="" disabled>
|
||||
— select a jail —
|
||||
</option>
|
||||
{activeJails.map((j) => (
|
||||
<option key={j.name} value={j.name}>
|
||||
{j.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Checkbox
|
||||
label="Reload fail2ban after assigning"
|
||||
checked={reload}
|
||||
onChange={(_e, d) => { setReload(Boolean(d.checked)); }}
|
||||
style={{ marginTop: tokens.spacingVerticalS }}
|
||||
/>
|
||||
</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 ? "Assigning…" : "Assign"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
<Checkbox
|
||||
label="Reload fail2ban after assigning"
|
||||
checked={reload}
|
||||
onChange={(_e, d) => { setReload(Boolean(d.checked)); }}
|
||||
style={{ marginTop: tokens.spacingVerticalS }}
|
||||
/>
|
||||
</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 ? "Assigning…" : "Assign"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,9 +54,8 @@ export interface AssignFilterDialogProps {
|
||||
/**
|
||||
* Confirmation dialog for assigning a filter to a jail.
|
||||
*
|
||||
* Fetches running jails from the API for the jail selector. The user can
|
||||
* optionally request a fail2ban reload immediately after the assignment is
|
||||
* written.
|
||||
* Wraps AssignFilterDialogInner and only renders the inner component when
|
||||
* the dialog is open, preventing unnecessary API calls when closed.
|
||||
*
|
||||
* @param props - Component props.
|
||||
* @returns JSX element.
|
||||
@@ -68,6 +67,43 @@ export function AssignFilterDialog({
|
||||
onAssigned,
|
||||
onAssign,
|
||||
}: AssignFilterDialogProps): React.JSX.Element {
|
||||
const handleClose = useCallback((): void => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(_e, data) => { if (!data.open) handleClose(); }}>
|
||||
<DialogSurface>
|
||||
{open && (
|
||||
<AssignFilterDialogInner
|
||||
filterName={filterName}
|
||||
onClose={handleClose}
|
||||
onAssigned={onAssigned}
|
||||
onAssign={onAssign}
|
||||
/>
|
||||
)}
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
interface AssignFilterDialogInnerProps {
|
||||
filterName: string | null;
|
||||
onClose: () => void;
|
||||
onAssigned: () => void;
|
||||
onAssign: (jailName: string, payload: AssignFilterRequest, reload: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner component that contains the dialog body and only renders when open.
|
||||
* This ensures useJails() is only called when the dialog is actually visible.
|
||||
*/
|
||||
function AssignFilterDialogInner({
|
||||
filterName,
|
||||
onClose,
|
||||
onAssigned,
|
||||
onAssign,
|
||||
}: AssignFilterDialogInnerProps): React.JSX.Element {
|
||||
const { jails, loading: jailsLoading, error: jailsError } = useJails();
|
||||
const [selectedJail, setSelectedJail] = useState("");
|
||||
const [reload, setReload] = useState(false);
|
||||
@@ -77,17 +113,16 @@ export function AssignFilterDialog({
|
||||
const activeJails = jails.filter((j) => j.enabled);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setError(null);
|
||||
setSelectedJail("");
|
||||
setReload(false);
|
||||
}, [open]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (jailsError && open) {
|
||||
if (jailsError) {
|
||||
setError(jailsError);
|
||||
}
|
||||
}, [jailsError, open]);
|
||||
}, [jailsError]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
if (submitting) return;
|
||||
@@ -119,87 +154,83 @@ export function AssignFilterDialog({
|
||||
const canConfirm = selectedJail !== "" && !submitting && !jailsLoading;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(_e, data) => { if (!data.open) handleClose(); }}>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>Assign Filter to Jail</DialogTitle>
|
||||
<DialogContent>
|
||||
{filterName !== null && (
|
||||
<Text
|
||||
as="p"
|
||||
size={300}
|
||||
style={{ marginBottom: tokens.spacingVerticalM }}
|
||||
>
|
||||
Assign filter{" "}
|
||||
<Text weight="semibold" style={{ fontFamily: "monospace" }}>
|
||||
{filterName}
|
||||
</Text>{" "}
|
||||
to a jail. This writes{" "}
|
||||
<Text style={{ fontFamily: "monospace" }}>filter = {filterName}</Text>{" "}
|
||||
into the jail's <code>.local</code> override file.
|
||||
</Text>
|
||||
)}
|
||||
<DialogBody>
|
||||
<DialogTitle>Assign Filter to Jail</DialogTitle>
|
||||
<DialogContent>
|
||||
{filterName !== null && (
|
||||
<Text
|
||||
as="p"
|
||||
size={300}
|
||||
style={{ marginBottom: tokens.spacingVerticalM }}
|
||||
>
|
||||
Assign filter{" "}
|
||||
<Text weight="semibold" style={{ fontFamily: "monospace" }}>
|
||||
{filterName}
|
||||
</Text>{" "}
|
||||
to a jail. This writes{" "}
|
||||
<Text style={{ fontFamily: "monospace" }}>filter = {filterName}</Text>{" "}
|
||||
into the jail's <code>.local</code> override file.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{error !== null && (
|
||||
<MessageBar
|
||||
intent="error"
|
||||
style={{ marginBottom: tokens.spacingVerticalS }}
|
||||
>
|
||||
<MessageBarBody>{error}</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
{error !== null && (
|
||||
<MessageBar
|
||||
intent="error"
|
||||
style={{ marginBottom: tokens.spacingVerticalS }}
|
||||
>
|
||||
<MessageBarBody>{error}</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label="Target jail"
|
||||
required
|
||||
hint="Only currently enabled jails are listed."
|
||||
<Field
|
||||
label="Target jail"
|
||||
required
|
||||
hint="Only currently enabled jails are listed."
|
||||
>
|
||||
{jailsLoading ? (
|
||||
<Spinner size="extra-small" label="Loading jails…" />
|
||||
) : (
|
||||
<Select
|
||||
value={selectedJail}
|
||||
onChange={(_e, d) => { setSelectedJail(d.value); }}
|
||||
aria-label="Target jail"
|
||||
>
|
||||
{jailsLoading ? (
|
||||
<Spinner size="extra-small" label="Loading jails…" />
|
||||
) : (
|
||||
<Select
|
||||
value={selectedJail}
|
||||
onChange={(_e, d) => { setSelectedJail(d.value); }}
|
||||
aria-label="Target jail"
|
||||
>
|
||||
<option value="" disabled>
|
||||
— select a jail —
|
||||
</option>
|
||||
{activeJails.map((j) => (
|
||||
<option key={j.name} value={j.name}>
|
||||
{j.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</Field>
|
||||
<option value="" disabled>
|
||||
— select a jail —
|
||||
</option>
|
||||
{activeJails.map((j) => (
|
||||
<option key={j.name} value={j.name}>
|
||||
{j.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Checkbox
|
||||
label="Reload fail2ban after assigning"
|
||||
checked={reload}
|
||||
onChange={(_e, d) => { setReload(Boolean(d.checked)); }}
|
||||
style={{ marginTop: tokens.spacingVerticalS }}
|
||||
/>
|
||||
</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 ? "Assigning…" : "Assign"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
<Checkbox
|
||||
label="Reload fail2ban after assigning"
|
||||
checked={reload}
|
||||
onChange={(_e, d) => { setReload(Boolean(d.checked)); }}
|
||||
style={{ marginTop: tokens.spacingVerticalS }}
|
||||
/>
|
||||
</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 ? "Assigning…" : "Assign"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,9 @@ function renderDialog(overrides: Partial<React.ComponentProps<typeof AssignFilte
|
||||
open: true,
|
||||
onClose: vi.fn(),
|
||||
onAssigned: vi.fn(),
|
||||
onAssign: vi.fn(async () => undefined),
|
||||
onAssign: vi.fn(async (jailName, payload, reload) => {
|
||||
await assignFilterToJail(jailName, payload, reload);
|
||||
}),
|
||||
...overrides,
|
||||
};
|
||||
return render(
|
||||
|
||||
Reference in New Issue
Block a user