fix(ui): блокировать операции Firewall после ошибки чтения

This commit is contained in:
Denozordec committed 2026-10-04 16:29:31 +07:00
1 parent 1e9889f892
commit 2e648dcf1c
6 files changed
+88 -19

No files matched your search

+25 -18
View File
@@ -1936,6 +1936,7 @@ function FirewallPageInner() {
const [dataLoading, setDataLoading] = useState(false)
const [dataError, setDataError] = useState<string | null>(null)
const [hasLoaded, setHasLoaded] = useState(false)
const [readTrusted, setReadTrusted] = useState(false)
const [sliceTimes, setSliceTimes] = useState<FirewallSliceTime[]>([])
const [failures, setFailures] = useState<FirewallFailure[]>([])
const loadEpoch = useRef(0)
@@ -1995,8 +1996,10 @@ function FirewallPageInner() {
const receivedAt = new Date().toISOString()
setSliceTimes(previous => updateFirewallSliceTimes(previous, [...enabledIds], firewall.data.failures, receivedAt))
setHasLoaded(true)
setReadTrusted(true)
} catch (e) {
if (epoch !== loadEpoch.current) return
setReadTrusted(false)
setDataError(e instanceof Error ? e.message : "Ошибка загрузки")
} finally {
if (epoch === loadEpoch.current) setDataLoading(false)
@@ -2029,6 +2032,7 @@ function FirewallPageInner() {
const restoreRevision = useCallback(async (id: string) => {
if (!isLive || !historyServerId) throw new Error("Выберите устройство в режиме Live")
if (mutationLock.current) throw new Error("Дождитесь завершения текущей операции Firewall")
if (!readTrusted) throw new Error("Обновите актуальные данные Firewall перед восстановлением")
if (failures.some(f => f.serverId === historyServerId)) throw new Error("Обновите полный набор данных устройства перед восстановлением")
mutationLock.current = true
loadEpoch.current += 1
@@ -2050,7 +2054,7 @@ function FirewallPageInner() {
mutationLock.current = false
setHistoryRestoring(false)
}
}, [isLive, historyServerId, apiFetch, loadLive, loadRevisions, failures])
}, [isLive, historyServerId, apiFetch, loadLive, loadRevisions, failures, readTrusted])
useEffect(() => {
queueMicrotask(() => {
@@ -2156,6 +2160,7 @@ function FirewallPageInner() {
const scopedFailures = isLive ? failures.filter(f => (selectedServerId === ALL_SERVERS_ID || f.serverId === selectedServerId) && (!f.family || f.family === ipFamily)) : []
const firewallIncomplete = scopedFailures.length > 0
const firewallWriteBlocked = firewallIncomplete || (isLive && !readTrusted)
const scopedSliceTimes = sliceTimes.filter(slice => (selectedServerId === ALL_SERVERS_ID || slice.serverId === selectedServerId) && slice.family === ipFamily)
const freshness = firewallTimeRange(scopedSliceTimes)
const totalEnabled = familyRules.filter((r) => r.enabled).length
@@ -2171,7 +2176,7 @@ function FirewallPageInner() {
async function toggleRule(id: string) {
const r = displayRules.find((x) => x.id === id)
if (!r || mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (!r || mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
if (isLive) {
const key = ruleKey(r)
if (!key) return
@@ -2202,13 +2207,13 @@ function FirewallPageInner() {
}
function requestDelete(target: NonNullable<typeof pendingDelete>) {
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
setDeleteError(null)
setPendingDelete(target)
}
async function confirmDelete() {
if (!pendingDelete || mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (!pendingDelete || mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
const target = pendingDelete
mutationLock.current = true
loadEpoch.current += 1
@@ -2248,7 +2253,7 @@ function FirewallPageInner() {
}, [filteredRules])
async function reorderRules(activeId: string, overId: string) {
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
const source = isLive ? liveRules : mockRules
const next = reorderWithinGroup(source, activeId, overId)
if (!next) {
@@ -2297,7 +2302,7 @@ function FirewallPageInner() {
}
function openAdd() {
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
setFormError(null)
if (!requireWriteServer()) return
const table = tableOfGroup(chainGroup)
@@ -2309,14 +2314,14 @@ function FirewallPageInner() {
}
function openEdit(r: FirewallRule) {
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
setFormError(null)
setEditingRule(r)
setRuleSheetOpen(true)
}
async function saveRule(payload: RuleWritePayload) {
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
const table = (editingRule?.table ?? tableOfGroup(chainGroup) ?? "filter") as FirewallTable
const family = editingRule?.id ? ruleFamily(editingRule as FirewallRule) : ipFamily
const serverId = editingRule?.serverId ?? requireWriteServer()
@@ -2387,7 +2392,7 @@ function FirewallPageInner() {
}
function openAddAddr(list?: string) {
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
setFormError(null)
if (!requireWriteServer()) return
setEditingAddr(list ? { list } : null)
@@ -2395,14 +2400,14 @@ function FirewallPageInner() {
}
function openEditAddr(e: AddressListEntry) {
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
setFormError(null)
setEditingAddr(e)
setAddrSheetOpen(true)
}
async function toggleAddr(e: AddressListEntry) {
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
if (isLive) {
const key = addressKey(e)
if (!key) return
@@ -2439,7 +2444,7 @@ function FirewallPageInner() {
timeout?: string
disabled?: boolean
}) {
if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return
if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return
const family = editingAddr?.id ? addressFamily(editingAddr as AddressListEntry) : ipFamily
const serverId = editingAddr?.serverId ?? requireWriteServer()
if (!serverId) return
@@ -2540,7 +2545,7 @@ function FirewallPageInner() {
setHistoryOpen(true)
void loadRevisions()
}}
disabled={!isLive || !historyServerId || dataLoading || busy || togglePending || deletePending || movePending || historyRestoring || failures.some(f => f.serverId === historyServerId)}
disabled={!isLive || !historyServerId || dataLoading || busy || togglePending || deletePending || movePending || historyRestoring || !readTrusted || failures.some(f => f.serverId === historyServerId)}
title={!historyServerId ? "Выберите сервер, чтобы смотреть историю" : "История версий и откат на CHR"}
>
<HistoryIcon className="size-4" />
@@ -2549,7 +2554,7 @@ function FirewallPageInner() {
<Button variant="outline" size="sm" onClick={() => setExportOpen(true)}>
<CodeXmlIcon className="size-4" />Экспорт .rsc
</Button>
<Button size="sm" disabled={firewallIncomplete} onClick={openAdd}>
<Button size="sm" disabled={firewallWriteBlocked} onClick={openAdd}>
<PlusIcon className="size-4" />Новое правило
</Button>
</>
@@ -2565,6 +2570,8 @@ function FirewallPageInner() {
</div>
)}
{isLive && hasLoaded && !readTrusted && <p role="status" className="text-sm text-muted-foreground">Изменения Firewall заблокированы до успешного чтения актуальных данных. Последний список сохранён.</p>}
<div className="flex items-center gap-3 flex-wrap">
<SegmentedControl
value={ipFamily}
@@ -2666,7 +2673,7 @@ function FirewallPageInner() {
) : chainGroup === "address-lists" ? (
<AddressListsTab
entries={familyAddrLists}
mutationsDisabled={togglePending || deletePending || movePending || busy || historyRestoring || firewallIncomplete}
mutationsDisabled={togglePending || deletePending || movePending || busy || historyRestoring || firewallWriteBlocked}
onAdd={() => openAddAddr()}
onAddToList={(list) => openAddAddr(list)}
onToggle={(e) => { void toggleAddr(e) }}
@@ -2708,11 +2715,11 @@ function FirewallPageInner() {
<FirewallRulesDataGrid
rules={filteredRules}
mutationsDisabled={togglePending || deletePending || movePending || busy || historyRestoring || firewallIncomplete}
mutationsDisabled={togglePending || deletePending || movePending || busy || historyRestoring || firewallWriteBlocked}
onToggle={(id) => { void toggleRule(id) }}
onEdit={openEdit}
onDelete={(item) => requestDelete({ kind: "rule", item })}
onReorder={canReorder && !togglePending && !deletePending && !movePending && !busy && !historyRestoring && !firewallIncomplete ? (activeId, overId) => { void reorderRules(activeId, overId) } : undefined}
onReorder={canReorder && !togglePending && !deletePending && !movePending && !busy && !historyRestoring && !firewallWriteBlocked ? (activeId, overId) => { void reorderRules(activeId, overId) } : undefined}
showServer={showServerCol}
/>
</DataPageCard>
@@ -2807,7 +2814,7 @@ function FirewallPageInner() {
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deletePending}>Отмена</AlertDialogCancel>
<AlertDialogAction variant="destructive" disabled={deletePending || togglePending || busy || historyRestoring || firewallIncomplete} onClick={e => { e.preventDefault(); void confirmDelete() }}>Удалить</AlertDialogAction>
<AlertDialogAction variant="destructive" disabled={deletePending || togglePending || busy || historyRestoring || firewallWriteBlocked} onClick={e => { e.preventDefault(); void confirmDelete() }}>Удалить</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>