fix(ui): сохранять данные Firewall при сбоях загрузки
This commit is contained in:
1 parent
92ba474ca9
commit
59440555cd
6 files changed
+173
-47
No files matched your search
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user