fix(ui): сохранить контекст и фокус форм Firewall без эффектов

This commit is contained in:
Denozordec committed 2026-10-04 16:56:15 +07:00
1 parent 21dc646553
commit ab56225a1a
6 files changed
+116 -75

No files matched your search

+51 -74
View File
@@ -323,34 +323,6 @@ const CHAIN_GROUPS: {
{ id: "simulator", label: "Симулятор", icon: <PlayIcon className="size-3.5" />, chains: [] },
]
// ─── Action styles ────────────────────────────────────────────────────────────
const ACTION_STYLES: Record<string, string> = {
accept: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20",
drop: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20",
reject: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20",
masquerade: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20",
"mark-routing": "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20",
"mark-conn": "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20",
"fasttrack-connection": "bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20",
"dst-nat": "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20",
"src-nat": "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20",
"add-src-to-address-list": "bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20",
}
const CHAIN_STYLES: Record<string, string> = {
forward: "bg-foreground/5 text-foreground/70",
input: "bg-violet-500/10 text-violet-600 dark:text-violet-400",
output: "bg-sky-500/10 text-sky-600 dark:text-sky-400",
srcnat: "bg-blue-500/10 text-blue-600 dark:text-blue-400",
dstnat: "bg-blue-500/10 text-blue-600 dark:text-blue-400",
prerouting: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
postrouting: "bg-amber-500/10 text-amber-600 dark:text-amber-400",
"ip6-input": "bg-violet-500/10 text-violet-600 dark:text-violet-400",
"ip6-forward": "bg-foreground/5 text-foreground/70",
"ip6-output": "bg-sky-500/10 text-sky-600 dark:text-sky-400",
}
// ─── Default form ─────────────────────────────────────────────────────────────
const defaultForm = {
@@ -488,7 +460,7 @@ function generateRsc(rules: FirewallRule[], timestamp?: string): string {
// ─── Rule Sheet ───────────────────────────────────────────────────────────────
function RuleSheet({ open, onClose, initialRule, chainGroup, family, onSave, saving = false, error }: {
function RuleSheet({ open, onClose, initialRule, chainGroup, family, onSave, saving = false, error, returnFocus }: {
open: boolean
onClose: () => void
initialRule: Partial<FirewallRule> | null
@@ -497,27 +469,32 @@ function RuleSheet({ open, onClose, initialRule, chainGroup, family, onSave, sav
onSave: (payload: RuleWritePayload) => void
saving?: boolean
error?: string | null
returnFocus?: () => HTMLElement | null
}) {
const isNew = !initialRule?.id
const [form, setForm] = useState<RuleForm>(defaultForm)
const [formContext, setFormContext] = useState({ open: false, initialRule, chainGroup })
useEffect(() => {
if (!open) return
setForm({
...defaultForm,
chain: initialRule?.chain ?? (CHAIN_GROUPS.find(g => g.id === chainGroup)?.chains[0] ?? "forward"),
action: (initialRule?.action as string) ?? defaultForm.action,
proto: initialRule?.proto ?? defaultForm.proto,
comment: initialRule?.comment ?? "",
enabled: initialRule?.enabled ?? true,
srcAddrList: initialRule?.src && initialRule.src !== "—" ? initialRule.src : "",
dstAddrList: initialRule?.dst && initialRule.dst !== "—" ? initialRule.dst : "",
dstPort: initialRule?.port && initialRule.port !== "—" ? initialRule.port : "",
inIface: initialRule?.iface && initialRule.iface !== "—" ? initialRule.iface : "",
log: initialRule?.log ?? false,
logPrefix: initialRule?.logPrefix ?? "",
})
}, [open, initialRule, chainGroup])
// Adjust this component before committing children with a stale draft.
if (formContext.open !== open || formContext.initialRule !== initialRule || formContext.chainGroup !== chainGroup) {
setFormContext({ open, initialRule, chainGroup })
if (open) {
setForm({
...defaultForm,
chain: initialRule?.chain ?? (CHAIN_GROUPS.find(g => g.id === chainGroup)?.chains[0] ?? "forward"),
action: (initialRule?.action as string) ?? defaultForm.action,
proto: initialRule?.proto ?? defaultForm.proto,
comment: initialRule?.comment ?? "",
enabled: initialRule?.enabled ?? true,
srcAddrList: initialRule?.src && initialRule.src !== "—" ? initialRule.src : "",
dstAddrList: initialRule?.dst && initialRule.dst !== "—" ? initialRule.dst : "",
dstPort: initialRule?.port && initialRule.port !== "—" ? initialRule.port : "",
inIface: initialRule?.iface && initialRule.iface !== "—" ? initialRule.iface : "",
log: initialRule?.log ?? false,
logPrefix: initialRule?.logPrefix ?? "",
})
}
}
const set = <K extends keyof RuleForm>(k: K, v: RuleForm[K]) =>
setForm((f) => ({ ...f, [k]: v }))
@@ -534,7 +511,7 @@ function RuleSheet({ open, onClose, initialRule, chainGroup, family, onSave, sav
return (
<Sheet open={open} onOpenChange={(v) => { if (!v && !saving) onClose() }}>
<SheetContent showCloseButton={!saving} className="flex flex-col overflow-hidden p-0 gap-0 sm:max-w-lg">
<SheetContent finalFocus={returnFocus} showCloseButton={!saving} className="flex flex-col overflow-hidden p-0 gap-0 sm:max-w-lg">
<SheetHeader className="shrink-0 px-6 pt-5 pb-4 border-b">
<SheetTitle>{isNew ? "Новое правило" : "Редактировать правило"}</SheetTitle>
<SheetDescription>
@@ -750,9 +727,7 @@ function AddressListsTab({
return ["all", ...Array.from(s)]
}, [entries])
useEffect(() => {
if (listFilter !== "all" && !lists.includes(listFilter)) setListFilter("all")
}, [lists, listFilter])
if (listFilter !== "all" && !lists.includes(listFilter)) setListFilter("all")
const filtered = useMemo(() => {
return entries.filter((e) => {
@@ -793,7 +768,7 @@ function AddressListsTab({
))}
</div>
<span className="text-sm text-muted-foreground ml-auto">{filtered.length} записей</span>
<Button size="sm" disabled={mutationsDisabled} onClick={onAdd}><PlusIcon className="size-4" />Добавить</Button>
<Button data-firewall-add-address size="sm" disabled={mutationsDisabled} onClick={onAdd}><PlusIcon className="size-4" />Добавить</Button>
</div>
<div className="flex flex-col gap-3">
@@ -812,7 +787,7 @@ function AddressListsTab({
</div>
<div className="divide-y divide-border">
{listEntries.map((e) => (
<div key={e.id} className={cn(
<div key={e.id} data-firewall-address-id={e.id} className={cn(
"flex items-center gap-3 px-4 py-2.5",
e.disabled && "opacity-50",
)}>
@@ -865,7 +840,7 @@ const defaultAddrForm = {
}
function AddressListSheet({
open, onClose, initial, family, onSave, saving = false, error,
open, onClose, initial, family, onSave, saving = false, error, returnFocus,
}: {
open: boolean
onClose: () => void
@@ -874,24 +849,28 @@ function AddressListSheet({
onSave: (payload: { list: string; address: string; comment?: string; timeout?: string; disabled?: boolean }) => void
saving?: boolean
error?: string | null
returnFocus?: () => HTMLElement | null
}) {
const isNew = !initial?.id
const [form, setForm] = useState(defaultAddrForm)
const [formContext, setFormContext] = useState({ open: false, initial })
useEffect(() => {
if (!open) return
setForm({
list: initial?.list ?? "",
address: initial?.address ?? "",
comment: initial?.comment ?? "",
timeout: initial?.timeout ?? "",
enabled: !(initial?.disabled ?? false),
})
}, [open, initial])
if (formContext.open !== open || formContext.initial !== initial) {
setFormContext({ open, initial })
if (open) {
setForm({
list: initial?.list ?? "",
address: initial?.address ?? "",
comment: initial?.comment ?? "",
timeout: initial?.timeout ?? "",
enabled: !(initial?.disabled ?? false),
})
}
}
return (
<Sheet open={open} onOpenChange={(v) => { if (!v && !saving) onClose() }}>
<SheetContent showCloseButton={!saving} className="flex flex-col overflow-hidden p-0 gap-0 sm:max-w-md">
<SheetContent finalFocus={returnFocus} showCloseButton={!saving} className="flex flex-col overflow-hidden p-0 gap-0 sm:max-w-md">
<SheetHeader className="shrink-0 px-6 pt-5 pb-4 border-b">
<SheetTitle>{isNew ? "Запись address-list" : "Редактировать запись"}</SheetTitle>
<SheetDescription>
@@ -1986,7 +1965,7 @@ function FirewallPageInner() {
const [deleteError, setDeleteError] = useState<string | null>(null)
const [selectedServerId, setSelectedServerId] = useState(serverFromQuery ?? ALL_SERVERS_ID)
const appliedQueryRef = useRef(false)
const [queryApplied, setQueryApplied] = useState(false)
const [chainGroup, setChainGroup] = useState<ChainGroup>("filter")
const [chainFilter, setChainFilter] = useState<ChainFilter>("all")
@@ -2109,14 +2088,10 @@ function FirewallPageInner() {
const displayAddrLists = isLive ? liveAddrLists : mockAddrLists
const displayServers = isLive ? liveServers : mockServers
useEffect(() => {
if (appliedQueryRef.current || !serverFromQuery) return
if (displayServers.length === 0) return
if (displayServers.some((s) => s.id === serverFromQuery)) {
setSelectedServerId(serverFromQuery)
}
appliedQueryRef.current = true
}, [displayServers, serverFromQuery])
if (!queryApplied && serverFromQuery && displayServers.length > 0) {
if (displayServers.some(server => server.id === serverFromQuery)) setSelectedServerId(serverFromQuery)
setQueryApplied(true)
}
const scopedRules = useMemo(() => {
if (selectedServerId === ALL_SERVERS_ID) return displayRules
@@ -2585,7 +2560,7 @@ function FirewallPageInner() {
<Button variant="outline" size="sm" onClick={() => setExportOpen(true)}>
<CodeXmlIcon className="size-4" />Экспорт .rsc
</Button>
<Button size="sm" disabled={firewallWriteBlocked} onClick={openAdd}>
<Button data-firewall-new-rule size="sm" disabled={firewallWriteBlocked} onClick={openAdd}>
<PlusIcon className="size-4" />Новое правило
</Button>
</>
@@ -2805,6 +2780,7 @@ function FirewallPageInner() {
<RuleSheet
open={ruleSheetOpen}
onClose={() => { if (!mutationLock.current) setRuleSheetOpen(false) }}
returnFocus={() => (editingRule?.id ? document.querySelector<HTMLButtonElement>(`tr[data-row-id="${CSS.escape(editingRule.id)}"] button[aria-haspopup="menu"]`) : null) ?? document.querySelector<HTMLButtonElement>("[data-firewall-new-rule]")}
initialRule={editingRule}
chainGroup={sheetChainGroup}
family={editingRule?.id ? ruleFamily(editingRule as FirewallRule) : ipFamily}
@@ -2816,6 +2792,7 @@ function FirewallPageInner() {
<AddressListSheet
open={addrSheetOpen}
onClose={() => { if (!mutationLock.current) setAddrSheetOpen(false) }}
returnFocus={() => (editingAddr?.id ? document.querySelector<HTMLButtonElement>(`[data-firewall-address-id="${CSS.escape(editingAddr.id)}"] button[aria-haspopup="menu"]`) : null) ?? document.querySelector<HTMLButtonElement>("[data-firewall-add-address]")}
initial={editingAddr}
family={editingAddr?.id ? addressFamily(editingAddr as AddressListEntry) : ipFamily}
onSave={(payload) => { void saveAddr(payload) }}