fix(ui): изолировать источники данных Filters

This commit is contained in:
Denozordec committed 2026-09-30 12:05:28 +07:00
1 parent 24cc66d8d2
commit 365ab2173c
7 files changed
+146 -9

No files matched your search

+26 -6
View File
@@ -1263,8 +1263,13 @@ function makeApiFetch(backendUrl: string) {
}
}
export default function FiltersPage() {
function FiltersPage() {
const { mode, backendUrl } = useDataSource()
const mounted = useRef(true)
useEffect(() => {
mounted.current = true
return () => { mounted.current = false }
}, [])
const evo = useEvoBGP()
/** Как на `/servers`: живые данные при режиме «Живые», без ожидания отдельного health-check */
const isLive = mode === "live"
@@ -1309,25 +1314,29 @@ export default function FiltersPage() {
const [loadAttempt, setLoadAttempt] = useState(0)
const ensureGreTunnels = useCallback(async (serverId: string) => {
if (!isLive) return
if (!isLive || !mounted.current) return
try {
const d = await apiFetch<{ tunnels: GreTunnel[] }>(
`/api/filters/gre-tunnels?serverId=${encodeURIComponent(serverId)}`,
)
if (!mounted.current) return
setGreByServer(prev => ({ ...prev, [serverId]: d.tunnels }))
} catch {
if (!mounted.current) return
setGreByServer(prev => ({ ...prev, [serverId]: [] }))
}
}, [isLive, apiFetch])
const ensureRecursiveRoutes = useCallback(async (serverId: string) => {
if (!isLive) return
if (!isLive || !mounted.current) return
try {
const d = await apiFetch<{ routes: RecursiveRouteLite[] }>(
`/api/recursive-routes?serverId=${encodeURIComponent(serverId)}`,
)
if (!mounted.current) return
setRecRoutesByServer(prev => ({ ...prev, [serverId]: d.routes }))
} catch {
if (!mounted.current) return
setRecRoutesByServer(prev => ({ ...prev, [serverId]: [] }))
}
}, [isLive, apiFetch])
@@ -1423,7 +1432,7 @@ export default function FiltersPage() {
}, [isLive, apiFetch, loadAttempt])
const loadLiveRules = useCallback(async (serverId: string) => {
if (!isLive || !serverId) return false
if (!isLive || !serverId || !mounted.current) return false
const epoch = ++readEpoch.current
setReadyServerId(null)
setRulesReading(true)
@@ -1521,6 +1530,7 @@ export default function FiltersPage() {
nextRules: FilterRule[],
source: "apply" | "copy" = "apply",
) => {
if (!mounted.current) return { ok: false, error: "Источник данных изменён" }
if (applyLock.current || applyBusy || historyRestoring) return { ok: false, error: "Дождитесь завершения текущей операции" }
if (isLive && (liveStale || liveLoadState !== "idle" || (serverId === selectedServerId && readyServerId !== serverId))) {
const error = "Роутер недоступен — изменения заблокированы"
@@ -1540,6 +1550,7 @@ export default function FiltersPage() {
method: "PUT",
body: JSON.stringify({ serverId, rules: nextRules, source }),
})
if (!mounted.current) return { ok: false, error: "Источник данных изменён" }
const parsed = filtersMutationSchema.safeParse(payload)
if (!parsed.success) throw new Error("Сервер не подтвердил применение правил")
const res = parsed.data
@@ -1547,6 +1558,7 @@ export default function FiltersPage() {
toast.success("Правила применены на роутер")
return { ok: true }
} catch (err) {
if (!mounted.current) return { ok: false, error: "Источник данных изменён" }
const error = err instanceof Error ? err.message : "Не удалось применить правила на роутер"
toast.error("Не удалось применить правила на роутер", { description: error })
return { ok: false, error }
@@ -1557,7 +1569,7 @@ export default function FiltersPage() {
}, [isLive, apiFetch, applyBusy, historyRestoring, liveStale, liveLoadState, readyServerId, selectedServerId])
const refreshFromRouter = useCallback(async () => {
if (!isLive || !selectedServerId || applyLock.current || applyBusy || historyRestoring) return
if (!isLive || !selectedServerId || !mounted.current || applyLock.current || applyBusy || historyRestoring) return
applyLock.current = true
setApplyBusy(true)
try {
@@ -1575,7 +1587,7 @@ export default function FiltersPage() {
useEffect(() => () => { historyEpoch.current += 1; readEpoch.current += 1 }, [selectedServerId, backendUrl, isLive])
const loadRevisions = useCallback(async () => {
if (!isLive || !selectedServerId) return
if (!isLive || !selectedServerId || !mounted.current) return
const epoch = ++historyEpoch.current
setHistoryLoading(true)
setHistoryError(null)
@@ -1606,6 +1618,7 @@ export default function FiltersPage() {
`/api/filters/revisions/${encodeURIComponent(id)}/restore`,
{ method: "POST", body: JSON.stringify({ serverId: selectedServerId }) },
)
if (!mounted.current) return
const parsed = filtersMutationSchema.safeParse(payload)
if (!parsed.success) throw new Error("Сервер не подтвердил восстановление")
const res = parsed.data
@@ -1614,9 +1627,11 @@ export default function FiltersPage() {
} else {
if (!await loadLiveRules(selectedServerId)) throw new Error("Версия применена, но актуальные правила не прочитаны — обновите данные")
}
if (!mounted.current) return
setLiveStale(false)
setReadyServerId(selectedServerId)
await loadRevisions()
if (!mounted.current) return
toast.success("Версия применена на роутер")
} finally {
applyLock.current = false
@@ -2048,3 +2063,8 @@ export default function FiltersPage() {
</>
)
}
export default function FiltersSource() {
const { mode, backendUrl } = useDataSource()
return <FiltersPage key={JSON.stringify([mode, backendUrl])} />
}