fix(ui): отклонять устаревшее чтение Filters
This commit is contained in:
1 parent
1c7ffa07a8
commit
24cc66d8d2
5 files changed
+104
-8
No files matched your search
@@ -1349,6 +1349,9 @@ export default function FiltersPage() {
|
||||
const [moveError, setMoveError] = useState<string | null>(null)
|
||||
const moveLock = useRef(false)
|
||||
const [liveStale, setLiveStale] = useState(false)
|
||||
const readEpoch = useRef(0)
|
||||
const [readyServerId, setReadyServerId] = useState<string | null>(null)
|
||||
const [rulesReading, setRulesReading] = useState(false)
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [historyLoading, setHistoryLoading] = useState(false)
|
||||
const [historyError, setHistoryError] = useState<string | null>(null)
|
||||
@@ -1371,6 +1374,8 @@ export default function FiltersPage() {
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
readEpoch.current += 1
|
||||
setReadyServerId(null)
|
||||
setLiveLoadState("loading")
|
||||
setLiveLoadError(null)
|
||||
Promise.all([
|
||||
@@ -1419,27 +1424,35 @@ export default function FiltersPage() {
|
||||
|
||||
const loadLiveRules = useCallback(async (serverId: string) => {
|
||||
if (!isLive || !serverId) return false
|
||||
const epoch = ++readEpoch.current
|
||||
setReadyServerId(null)
|
||||
setRulesReading(true)
|
||||
try {
|
||||
const payload = await apiFetch<unknown>(
|
||||
`/api/filters/rules?serverId=${encodeURIComponent(serverId)}`,
|
||||
)
|
||||
if (epoch !== readEpoch.current) return false
|
||||
const parsed = filtersResponseSchema.safeParse(payload)
|
||||
if (!parsed.success) throw new Error("Сервер вернул некорректные правила Filters")
|
||||
const fresh = parsed.data
|
||||
const ruleset = fresh.rulesets.find(r => r.serverId === serverId)
|
||||
if (!ruleset || fresh.stale) throw new Error("Не удалось получить актуальные правила выбранного устройства")
|
||||
if (!ruleset || fresh.stale || fresh.live !== true) throw new Error("Не удалось получить актуальные правила выбранного устройства")
|
||||
const liveRules = ruleset.rules
|
||||
setRulesets((prev) => {
|
||||
const has = prev.some((rs) => rs.serverId === serverId)
|
||||
if (!has) return [...prev, { serverId, rules: liveRules }]
|
||||
return prev.map((rs) => rs.serverId === serverId ? { ...rs, rules: liveRules } : rs)
|
||||
})
|
||||
setLiveStale(Boolean(fresh.stale))
|
||||
setLiveStale(false)
|
||||
setReadyServerId(serverId)
|
||||
return true
|
||||
} catch (err) {
|
||||
if (epoch !== readEpoch.current) return false
|
||||
setLiveStale(true)
|
||||
toast.error("Не удалось прочитать правила с роутера", { description: String(err) })
|
||||
return false
|
||||
} finally {
|
||||
if (epoch === readEpoch.current) setRulesReading(false)
|
||||
}
|
||||
}, [isLive, apiFetch])
|
||||
|
||||
@@ -1457,7 +1470,7 @@ export default function FiltersPage() {
|
||||
const allTunnels = !isLive ? greTunnels : (greByServer[selectedServerId] ?? [])
|
||||
const selectedServer = allServers.find(s => s.id === selectedServerId) ?? allServers[0]
|
||||
const totalRules = rulesets.reduce((s, r) => s + r.rules.length, 0)
|
||||
const mutationsLocked = movePending || (isLive && (applyBusy || historyRestoring || liveStale || liveLoadState !== "idle"))
|
||||
const mutationsLocked = movePending || (isLive && (applyBusy || historyRestoring || rulesReading || readyServerId !== selectedServerId || liveStale || liveLoadState !== "idle"))
|
||||
|
||||
const filterRailItems = useMemo<ServerTileItem[]>(() => (
|
||||
allServers.map((s) => ({
|
||||
@@ -1475,6 +1488,10 @@ export default function FiltersPage() {
|
||||
|
||||
const handleSelectServer = useCallback((id: string) => {
|
||||
if (applyLock.current) return
|
||||
readEpoch.current += 1
|
||||
setReadyServerId(null)
|
||||
setRulesReading(false)
|
||||
setLiveStale(false)
|
||||
setSheetOpen(false)
|
||||
setPendingDelete(null)
|
||||
setDeleteError(null)
|
||||
@@ -1505,7 +1522,7 @@ export default function FiltersPage() {
|
||||
source: "apply" | "copy" = "apply",
|
||||
) => {
|
||||
if (applyLock.current || applyBusy || historyRestoring) return { ok: false, error: "Дождитесь завершения текущей операции" }
|
||||
if (isLive && (liveStale || liveLoadState !== "idle")) {
|
||||
if (isLive && (liveStale || liveLoadState !== "idle" || (serverId === selectedServerId && readyServerId !== serverId))) {
|
||||
const error = "Роутер недоступен — изменения заблокированы"
|
||||
toast.error(error)
|
||||
return { ok: false, error }
|
||||
@@ -1515,6 +1532,8 @@ export default function FiltersPage() {
|
||||
return { ok: true }
|
||||
}
|
||||
applyLock.current = true
|
||||
readEpoch.current += 1
|
||||
setRulesReading(false)
|
||||
setApplyBusy(true)
|
||||
try {
|
||||
const payload = await apiFetch<unknown>("/api/filters/rules", {
|
||||
@@ -1535,7 +1554,7 @@ export default function FiltersPage() {
|
||||
applyLock.current = false
|
||||
setApplyBusy(false)
|
||||
}
|
||||
}, [isLive, apiFetch, applyBusy, historyRestoring, liveStale, liveLoadState])
|
||||
}, [isLive, apiFetch, applyBusy, historyRestoring, liveStale, liveLoadState, readyServerId, selectedServerId])
|
||||
|
||||
const refreshFromRouter = useCallback(async () => {
|
||||
if (!isLive || !selectedServerId || applyLock.current || applyBusy || historyRestoring) return
|
||||
@@ -1553,7 +1572,7 @@ export default function FiltersPage() {
|
||||
}
|
||||
}, [isLive, selectedServerId, applyBusy, historyRestoring, loadLiveRules, ensureGreTunnels, ensureRecursiveRoutes])
|
||||
|
||||
useEffect(() => () => { historyEpoch.current += 1 }, [selectedServerId, backendUrl, isLive])
|
||||
useEffect(() => () => { historyEpoch.current += 1; readEpoch.current += 1 }, [selectedServerId, backendUrl, isLive])
|
||||
|
||||
const loadRevisions = useCallback(async () => {
|
||||
if (!isLive || !selectedServerId) return
|
||||
@@ -1580,6 +1599,8 @@ export default function FiltersPage() {
|
||||
applyLock.current = true
|
||||
setApplyBusy(true)
|
||||
setHistoryRestoring(true)
|
||||
readEpoch.current += 1
|
||||
setRulesReading(false)
|
||||
try {
|
||||
const payload = await apiFetch<unknown>(
|
||||
`/api/filters/revisions/${encodeURIComponent(id)}/restore`,
|
||||
@@ -1594,6 +1615,7 @@ export default function FiltersPage() {
|
||||
if (!await loadLiveRules(selectedServerId)) throw new Error("Версия применена, но актуальные правила не прочитаны — обновите данные")
|
||||
}
|
||||
setLiveStale(false)
|
||||
setReadyServerId(selectedServerId)
|
||||
await loadRevisions()
|
||||
toast.success("Версия применена на роутер")
|
||||
} finally {
|
||||
@@ -1799,6 +1821,7 @@ export default function FiltersPage() {
|
||||
<Button variant="outline" size="sm" onClick={() => setLoadAttempt(n => n + 1)}>Повторить загрузку</Button>
|
||||
</div>
|
||||
)}
|
||||
{isLive && rulesReading && <p role="status" className="border-b px-6 py-2 text-xs text-muted-foreground">Чтение актуальных правил устройства… Изменения временно заблокированы.</p>}
|
||||
{isLive && liveStale && liveLoadState !== "error" && (
|
||||
<div className="shrink-0 border-b border-amber-500/30 bg-amber-500/10 px-6 py-2.5 text-xs text-amber-700 dark:text-amber-400 flex items-center gap-2">
|
||||
<AlertTriangleIcon className="size-3.5 shrink-0" />
|
||||
|
||||
Reference in new issue
Block a user