fix(ui): сохранять данные Firewall при сбоях загрузки

This commit is contained in:
Denozordec committed 2026-09-30 10:33:54 +07:00
1 parent 92ba474ca9
commit 59440555cd
6 files changed
+173 -47

No files matched your search

+48 -44
View File
@@ -21,6 +21,7 @@ import {
type Server,
} from "@/lib/data"
import { useDataSource } from "@/lib/data-source"
import { firewallResponseSchema, firewallServersSchema, type FirewallBackendServer } from "@/lib/firewall-response"
import { requestJson } from "@/shared/api/http-client"
import { Frame, FramePanel } from "@/components/reui/frame"
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
@@ -1797,31 +1798,13 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
// ─── Live API ─────────────────────────────────────────────────────────────────
interface BackendServer {
id: number
name: string
host: string
type: "jump-host" | "exit-node" | "home-router"
site: string
country: string
asn: string
enabled: boolean
status: "online" | "offline" | null
latency: number | null
}
interface FirewallAllResponse {
rules: FirewallRule[]
addressLists: AddressListEntry[]
}
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
return requestJson<T>(backendUrl, path, init)
}
}
function mapBackendToServer(s: BackendServer): Server {
function mapBackendToServer(s: FirewallBackendServer): Server {
return {
id: String(s.id),
name: s.name || s.host,
@@ -1946,6 +1929,9 @@ function FirewallPageInner() {
const [mockAddrLists, setMockAddrLists] = useState<AddressListEntry[]>(INIT_ADDRESS_LISTS)
const [dataLoading, setDataLoading] = useState(false)
const [dataError, setDataError] = useState<string | null>(null)
const [hasLoaded, setHasLoaded] = useState(false)
const loadEpoch = useRef(0)
useEffect(() => () => { loadEpoch.current += 1 }, [backendUrl, isLive])
const [busy, setBusy] = useState(false)
const [formError, setFormError] = useState<string | null>(null)
const mutationLock = useRef(false)
@@ -1981,23 +1967,27 @@ function FirewallPageInner() {
const loadLive = useCallback(async () => {
if (!isLive) return
const epoch = ++loadEpoch.current
setDataLoading(true)
setDataError(null)
try {
const [backendServers, fw] = await Promise.all([
apiFetch<BackendServer[]>("/api/servers"),
apiFetch<FirewallAllResponse>("/api/firewall/all"),
apiFetch<unknown>("/api/servers"),
apiFetch<unknown>("/api/firewall/all"),
])
setLiveServers(backendServers.map(mapBackendToServer))
setLiveRules(fw.rules)
setLiveAddrLists(fw.addressLists)
if (epoch !== loadEpoch.current) return
const servers = firewallServersSchema.safeParse(backendServers)
const firewall = firewallResponseSchema.safeParse(fw)
if (!servers.success || !firewall.success) throw new Error("Сервер вернул некорректные данные Firewall")
setLiveServers(servers.data.map(mapBackendToServer))
setLiveRules(firewall.data.rules)
setLiveAddrLists(firewall.data.addressLists)
setHasLoaded(true)
} catch (e) {
if (epoch !== loadEpoch.current) return
setDataError(e instanceof Error ? e.message : "Ошибка загрузки")
setLiveServers([])
setLiveRules([])
setLiveAddrLists([])
} finally {
setDataLoading(false)
if (epoch === loadEpoch.current) setDataLoading(false)
}
}, [isLive, apiFetch])
@@ -2028,6 +2018,8 @@ function FirewallPageInner() {
if (!isLive || !historyServerId) throw new Error("Выберите устройство в режиме Live")
if (mutationLock.current) throw new Error("Дождитесь завершения текущей операции Firewall")
mutationLock.current = true
loadEpoch.current += 1
setDataLoading(false)
setHistoryRestoring(true)
try {
const res = await apiFetch<{ ok: boolean }>(
@@ -2047,17 +2039,14 @@ function FirewallPageInner() {
}, [isLive, historyServerId, apiFetch, loadLive, loadRevisions])
useEffect(() => {
if (!isLive) {
queueMicrotask(() => {
setLiveServers([])
setLiveRules([])
setLiveAddrLists([])
setDataError(null)
})
return
}
queueMicrotask(() => {
void loadLive()
setLiveServers([])
setLiveRules([])
setLiveAddrLists([])
setHasLoaded(false)
setDataError(null)
setDataLoading(false)
if (isLive) void loadLive()
})
}, [isLive, loadLive])
@@ -2167,6 +2156,8 @@ function FirewallPageInner() {
const key = ruleKey(r)
if (!key) return
mutationLock.current = true
loadEpoch.current += 1
setDataLoading(false)
setTogglePending(true)
setToggleError(null)
setToggleStatus(`Ожидание изменения правила ${r.comment || r.rosId || r.id}…`)
@@ -2199,6 +2190,8 @@ function FirewallPageInner() {
if (!pendingDelete || mutationLock.current || busy || historyRestoring) return
const target = pendingDelete
mutationLock.current = true
loadEpoch.current += 1
setDataLoading(false)
setDeletePending(true)
setDeleteError(null)
try {
@@ -2253,6 +2246,8 @@ function FirewallPageInner() {
if (!dest) return
const prev = source
mutationLock.current = true
loadEpoch.current += 1
setDataLoading(false)
setMovePending(true)
setMoveError(null)
setMoveStatus("Ожидание перемещения правила " + (active?.comment || active?.rosId || activeId) + "…")
@@ -2309,6 +2304,8 @@ function FirewallPageInner() {
if (isLive) {
mutationLock.current = true
loadEpoch.current += 1
setDataLoading(false)
setBusy(true)
setFormError(null)
try {
@@ -2384,6 +2381,8 @@ function FirewallPageInner() {
const key = addressKey(e)
if (!key) return
mutationLock.current = true
loadEpoch.current += 1
setDataLoading(false)
setTogglePending(true)
setToggleError(null)
setToggleStatus(`Ожидание изменения адреса ${e.address} (${e.list})…`)
@@ -2424,6 +2423,8 @@ function FirewallPageInner() {
if (isLive) {
mutationLock.current = true
loadEpoch.current += 1
setDataLoading(false)
setBusy(true)
setFormError(null)
try {
@@ -2529,8 +2530,9 @@ function FirewallPageInner() {
>
<div className="flex flex-col gap-5">
{dataError && isLive && (
<div className="rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive">
<div role="alert" className="rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive">
{dataError}
{hasLoaded && <span> Показаны последние успешно загруженные данные.</span>}
</div>
)}
@@ -2558,28 +2560,28 @@ function FirewallPageInner() {
{
id: "rules",
label: "Всего правил",
value: familyRules.length,
value: isLive && !hasLoaded ? "—" : familyRules.length,
icon: <ShieldIcon className="size-4" />,
iconClassName: "text-muted-foreground",
},
{
id: "enabled",
label: "Активных",
value: totalEnabled,
value: isLive && !hasLoaded ? "—" : totalEnabled,
icon: <CheckCircleIcon className="size-4" />,
iconClassName: "text-success",
},
{
id: "drop",
label: "Блокирующих",
value: dropRules,
value: isLive && !hasLoaded ? "—" : dropRules,
icon: <ShieldOffIcon className="size-4" />,
iconClassName: "text-destructive",
},
{
id: "hits",
label: "Срабатываний",
value: fmtHits(totalHits),
value: isLive && !hasLoaded ? "—" : fmtHits(totalHits),
icon: <ListFilterIcon className="size-4" />,
iconClassName: "text-info",
},
@@ -2610,7 +2612,9 @@ function FirewallPageInner() {
))}
</div>
{chainGroup === "address-lists" ? (
{isLive && !hasLoaded ? (
<p role="status" className="text-sm text-muted-foreground">{dataLoading ? "Загрузка данных Firewall…" : "Данные Firewall пока недоступны. Повторите загрузку."}</p>
) : chainGroup === "address-lists" ? (
<AddressListsTab
entries={familyAddrLists}
mutationsDisabled={togglePending || deletePending || movePending || busy || historyRestoring}