fix(ui): блокировать операции Firewall после ошибки чтения
This commit is contained in:
1 parent
1e9889f892
commit
2e648dcf1c
6 files changed
+88
-19
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user