fix(ui): подтверждать удаление объектов Firewall
This commit is contained in:
1 parent
0c460d4767
commit
14a9e6d26c
6 files changed
+154
-38
No files matched your search
@@ -40,6 +40,10 @@ import {
|
||||
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
|
||||
DropdownMenuItem, DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle,
|
||||
AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
PlusIcon, SearchIcon, ShieldIcon, ShieldOffIcon,
|
||||
@@ -821,7 +825,7 @@ function AddressListsTab({
|
||||
<FormToggle checked={!e.disabled} disabled={mutationsDisabled} onChange={() => onToggle(e)} />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={
|
||||
<Button variant="ghost" size="icon" className="size-7 shrink-0">
|
||||
<Button variant="ghost" size="icon" className="size-7 shrink-0" aria-label={"Действия: " + e.address}>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
} />
|
||||
@@ -831,7 +835,7 @@ function AddressListsTab({
|
||||
<PowerIcon className="size-4" />{e.disabled ? "Включить" : "Отключить"}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive" onClick={() => onDelete(e)}>
|
||||
<DropdownMenuItem disabled={mutationsDisabled} variant="destructive" onClick={() => onDelete(e)}>
|
||||
<Trash2Icon className="size-4" />Удалить
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -1941,6 +1945,9 @@ function FirewallPageInner() {
|
||||
const [togglePending, setTogglePending] = useState(false)
|
||||
const [toggleStatus, setToggleStatus] = useState<string | null>(null)
|
||||
const [toggleError, setToggleError] = useState<string | null>(null)
|
||||
const [pendingDelete, setPendingDelete] = useState<{ kind: "rule"; item: FirewallRule } | { kind: "address"; item: AddressListEntry } | null>(null)
|
||||
const [deletePending, setDeletePending] = useState(false)
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null)
|
||||
|
||||
const [selectedServerId, setSelectedServerId] = useState(serverFromQuery ?? ALL_SERVERS_ID)
|
||||
const appliedQueryRef = useRef(false)
|
||||
@@ -2169,20 +2176,40 @@ function FirewallPageInner() {
|
||||
setMockRules((rs) => rs.map((x) => x.id === id ? { ...x, enabled: !x.enabled } : x))
|
||||
}
|
||||
|
||||
async function deleteRule(r: FirewallRule) {
|
||||
if (isLive) {
|
||||
const key = ruleKey(r)
|
||||
if (!key) return
|
||||
try {
|
||||
await apiFetch("/api/firewall/rules", { method: "DELETE", body: JSON.stringify(key) })
|
||||
toast.success("Правило удалено")
|
||||
function requestDelete(target: NonNullable<typeof pendingDelete>) {
|
||||
if (toggleLock.current || busy || historyRestoring) return
|
||||
setDeleteError(null)
|
||||
setPendingDelete(target)
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!pendingDelete || toggleLock.current || busy || historyRestoring) return
|
||||
const target = pendingDelete
|
||||
toggleLock.current = true
|
||||
setDeletePending(true)
|
||||
setDeleteError(null)
|
||||
try {
|
||||
if (isLive) {
|
||||
const key = target.kind === "rule" ? ruleKey(target.item) : addressKey(target.item)
|
||||
if (!key) throw new Error("Не удалось определить устройство и ID объекта")
|
||||
const result = await apiFetch<{ ok: boolean }>(target.kind === "rule" ? "/api/firewall/rules" : "/api/firewall/address-lists", {
|
||||
method: "DELETE", body: JSON.stringify(key),
|
||||
})
|
||||
if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление")
|
||||
await loadLive()
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Не удалось удалить правило")
|
||||
} else if (target.kind === "rule") {
|
||||
setMockRules(rows => rows.filter(x => x.id !== target.item.id))
|
||||
} else {
|
||||
setMockAddrLists(rows => rows.filter(x => x.id !== target.item.id))
|
||||
}
|
||||
return
|
||||
setPendingDelete(null)
|
||||
toast.success(target.kind === "rule" ? "Правило удалено" : "Запись удалена")
|
||||
} catch (e) {
|
||||
setDeleteError(e instanceof Error ? e.message : "Не удалось удалить объект")
|
||||
} finally {
|
||||
toggleLock.current = false
|
||||
setDeletePending(false)
|
||||
}
|
||||
setMockRules((rs) => rs.filter((x) => x.id !== r.id))
|
||||
}
|
||||
|
||||
const canReorder = useMemo(() => {
|
||||
@@ -2342,22 +2369,6 @@ function FirewallPageInner() {
|
||||
setMockAddrLists((rows) => rows.map((x) => x.id === e.id ? { ...x, disabled: !x.disabled } : x))
|
||||
}
|
||||
|
||||
async function deleteAddr(e: AddressListEntry) {
|
||||
if (isLive) {
|
||||
const key = addressKey(e)
|
||||
if (!key) return
|
||||
try {
|
||||
await apiFetch("/api/firewall/address-lists", { method: "DELETE", body: JSON.stringify(key) })
|
||||
toast.success("Запись удалена")
|
||||
await loadLive()
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Не удалось удалить запись")
|
||||
}
|
||||
return
|
||||
}
|
||||
setMockAddrLists((rows) => rows.filter((x) => x.id !== e.id))
|
||||
}
|
||||
|
||||
async function saveAddr(payload: {
|
||||
list: string
|
||||
address: string
|
||||
@@ -2556,12 +2567,12 @@ function FirewallPageInner() {
|
||||
{chainGroup === "address-lists" ? (
|
||||
<AddressListsTab
|
||||
entries={familyAddrLists}
|
||||
mutationsDisabled={togglePending || busy || historyRestoring}
|
||||
mutationsDisabled={togglePending || deletePending || busy || historyRestoring}
|
||||
onAdd={() => openAddAddr()}
|
||||
onAddToList={(list) => openAddAddr(list)}
|
||||
onToggle={(e) => { void toggleAddr(e) }}
|
||||
onEdit={openEditAddr}
|
||||
onDelete={(e) => { void deleteAddr(e) }}
|
||||
onDelete={(item) => requestDelete({ kind: "address", item })}
|
||||
showServer={showServerCol}
|
||||
/>
|
||||
) : chainGroup === "simulator" ? (
|
||||
@@ -2598,11 +2609,11 @@ function FirewallPageInner() {
|
||||
|
||||
<FirewallRulesDataGrid
|
||||
rules={filteredRules}
|
||||
mutationsDisabled={togglePending || busy || historyRestoring}
|
||||
mutationsDisabled={togglePending || deletePending || busy || historyRestoring}
|
||||
onToggle={(id) => { void toggleRule(id) }}
|
||||
onEdit={openEdit}
|
||||
onDelete={(r) => { void deleteRule(r) }}
|
||||
onReorder={canReorder && !togglePending ? (activeId, overId) => { void reorderRules(activeId, overId) } : undefined}
|
||||
onDelete={(item) => requestDelete({ kind: "rule", item })}
|
||||
onReorder={canReorder && !togglePending && !deletePending ? (activeId, overId) => { void reorderRules(activeId, overId) } : undefined}
|
||||
showServer={showServerCol}
|
||||
/>
|
||||
</DataPageCard>
|
||||
@@ -2679,6 +2690,27 @@ function FirewallPageInner() {
|
||||
rules={familyRules}
|
||||
/>
|
||||
|
||||
<AlertDialog open={!!pendingDelete} onOpenChange={open => {
|
||||
if (!open && !toggleLock.current) { setPendingDelete(null); setDeleteError(null) }
|
||||
}}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{pendingDelete?.kind === "rule" ? "Удалить правило Firewall?" : "Удалить запись address-list?"}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{pendingDelete && (pendingDelete.kind === "rule"
|
||||
? [pendingDelete.item.comment || pendingDelete.item.rosId || pendingDelete.item.id, pendingDelete.item.serverName || pendingDelete.item.serverId, ruleFamily(pendingDelete.item), pendingDelete.item.table, pendingDelete.item.chain].join(" · ")
|
||||
: [pendingDelete.item.address, pendingDelete.item.list, pendingDelete.item.serverName || pendingDelete.item.serverId, addressFamily(pendingDelete.item)].join(" · "))}
|
||||
</AlertDialogDescription>
|
||||
{deletePending && <p role="status" className="text-sm text-muted-foreground">Ожидание результата удаления…</p>}
|
||||
{deleteError && <p role="alert" className="text-sm text-destructive">{deleteError}</p>}
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deletePending}>Отмена</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" disabled={deletePending || togglePending || busy || historyRestoring} onClick={e => { e.preventDefault(); void confirmDelete() }}>Удалить</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<ConfigHistorySheet
|
||||
open={historyOpen}
|
||||
onOpenChange={v => { if (!v) historyEpoch.current += 1; setHistoryOpen(v) }}
|
||||
|
||||
Reference in new issue
Block a user