fix(ui): сохранить контекст и фокус форм Firewall без эффектов
This commit is contained in:
1 parent
21dc646553
commit
ab56225a1a
6 files changed
+116
-75
No files matched your search
@@ -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) }}
|
||||
|
||||
Reference in new issue
Block a user