Add editing of client access rules
Saved rules and client form drafts can now be edited through the rule modal. On the Rules tab, saving re-runs client setup so the firewall matches, and both add and edit ask for confirmation. Comma-separated destinations were rejected because drafts were validated before being split; validate each one after splitting instead. When editing, the first destination updates the rule and the rest are added as new rules. Pin the id in the client_rules PUT handler so a request body can't change it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,123 +1,44 @@
|
||||
import { useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import Modal from "@/src/components/twui/elements/Modal";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import H3 from "@/src/components/twui/layout/H3";
|
||||
import P from "@/src/components/twui/layout/P";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import InlineAlert from "../inline-alert";
|
||||
import ClientRuleFields from "./client-rule-fields";
|
||||
import buildClientRuleDrafts from "./build-client-rule-drafts";
|
||||
import { createClientRuleDraft, type ClientRuleDraft } from "./client-rule-draft";
|
||||
import ClientRuleModalBody from "./client-rule-modal-body";
|
||||
import ClientRuleModalTarget from "./client-rule-modal-target";
|
||||
import type { ClientRuleDraft } from "./client-rule-draft";
|
||||
|
||||
export type ClientRuleModalAddResult = { success: boolean; msg?: string };
|
||||
/** `cancelled` keeps the modal open without showing an error. */
|
||||
export type ClientRuleModalSaveResult = {
|
||||
success: boolean;
|
||||
msg?: string;
|
||||
cancelled?: boolean;
|
||||
};
|
||||
|
||||
export type ClientRuleModalSave = ({
|
||||
rules,
|
||||
}: {
|
||||
rules: ClientRuleDraft[];
|
||||
}) => Promise<ClientRuleModalSaveResult>;
|
||||
|
||||
type Props = {
|
||||
onAdd: ({ rules }: { rules: ClientRuleDraft[] }) => Promise<ClientRuleModalAddResult>;
|
||||
/** The rule to edit. Omit to add a new one. */
|
||||
rule?: ClientRuleDraft;
|
||||
onSave: ClientRuleModalSave;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/** Validates a rule and hands it to `onAdd`. Closes only on success. */
|
||||
export default function ClientRuleModal({ onAdd, disabled }: Props) {
|
||||
/**
|
||||
* Validates a rule and hands it to `onSave`. Closes only on success. Commas in
|
||||
* the destination yield several drafts, in both add and edit.
|
||||
*/
|
||||
export default function ClientRuleModal({ rule, onSave, disabled }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [draft, setDraft] = useState<ClientRuleDraft>(createClientRuleDraft());
|
||||
const [error, setError] = useState<string>();
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
function reset() {
|
||||
setDraft(createClientRuleDraft());
|
||||
setError(undefined);
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
const built = buildClientRuleDrafts({ draft });
|
||||
|
||||
if (!built.success) {
|
||||
setError(built.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
const res = await onAdd({ rules: built.drafts });
|
||||
setSaving(false);
|
||||
|
||||
if (!res.success) {
|
||||
setError(res.msg || "Couldn't add the rule");
|
||||
return;
|
||||
}
|
||||
|
||||
reset();
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
setOpen={(value) => {
|
||||
setOpen(value);
|
||||
if (value === false) reset();
|
||||
}}
|
||||
setOpen={setOpen}
|
||||
className="max-w-lg p-5"
|
||||
target={
|
||||
<Button
|
||||
title="Add rule"
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
>
|
||||
<Plus size={14} />
|
||||
Add rule
|
||||
</Button>
|
||||
}
|
||||
target={<ClientRuleModalTarget is_edit={Boolean(rule)} disabled={disabled} />}
|
||||
>
|
||||
<Stack className="w-full items-stretch gap-5">
|
||||
<Stack className="gap-1 pr-8">
|
||||
<H3 className="text-lg!">Add access rule</H3>
|
||||
<P className="text-[15px]">
|
||||
Allow this client to reach a destination. Leave ports empty for any port.
|
||||
</P>
|
||||
</Stack>
|
||||
|
||||
{error ? <InlineAlert tone="error" msg={error} /> : null}
|
||||
|
||||
<ClientRuleFields
|
||||
key={draft.local_id}
|
||||
draft={draft}
|
||||
setDraft={(next) => {
|
||||
setDraft(next);
|
||||
setError(undefined);
|
||||
}}
|
||||
onEnter={handleAdd}
|
||||
/>
|
||||
|
||||
<Row className="w-full justify-end gap-2 pt-1">
|
||||
<Button
|
||||
title="Cancel"
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
reset();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
title="Add rule"
|
||||
type="button"
|
||||
size="small"
|
||||
loading={saving}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
Add rule
|
||||
</Button>
|
||||
</Row>
|
||||
</Stack>
|
||||
<ClientRuleModalBody rule={rule} onSave={onSave} onClose={() => setOpen(false)} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user