fix(ui): проверять данные загрузки Filters
This commit is contained in:
1 parent
1fba16e1f2
commit
1c7ffa07a8
6 files changed
+143
-44
No files matched your search
+46
-42
@@ -14,6 +14,8 @@ import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { filtersResponseSchema, filtersMutationSchema } from "@/lib/filters-response"
|
||||
import { firewallServersSchema } from "@/lib/firewall-response"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import {
|
||||
servers, greTunnels, serverFilterRulesets, filters as mockFiltersCatalog,
|
||||
@@ -1248,27 +1250,6 @@ function CopyRulesSheet({
|
||||
|
||||
// ── page ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
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 LiveFiltersResponse {
|
||||
rulesets: ServerFilterRuleset[]
|
||||
greTunnels: GreTunnel[]
|
||||
live?: boolean
|
||||
stale?: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
function buildRulesets(serverList: Server[], sourceRulesets: ServerFilterRuleset[]): ServerFilterRuleset[] {
|
||||
return serverList.map(s => ({
|
||||
serverId: s.id,
|
||||
@@ -1318,12 +1299,15 @@ export default function FiltersPage() {
|
||||
const [greByServer, setGreByServer] = useState<Record<string, GreTunnel[]>>({})
|
||||
const [recRoutesByServer, setRecRoutesByServer] = useState<Record<string, RecursiveRouteLite[]>>({})
|
||||
const [rulesets, setRulesets] = useState<ServerFilterRuleset[]>(
|
||||
() => buildRulesets(servers, serverFilterRulesets),
|
||||
() => isLive ? [] : buildRulesets(servers, serverFilterRulesets),
|
||||
)
|
||||
const [liveLoadState, setLiveLoadState] = useState<"idle" | "loading" | "error">(
|
||||
() => (mode === "live" ? "loading" : "idle"),
|
||||
)
|
||||
|
||||
const [liveLoadError, setLiveLoadError] = useState<string | null>(null)
|
||||
const [loadAttempt, setLoadAttempt] = useState(0)
|
||||
|
||||
const ensureGreTunnels = useCallback(async (serverId: string) => {
|
||||
if (!isLive) return
|
||||
try {
|
||||
@@ -1386,12 +1370,21 @@ export default function FiltersPage() {
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
setLiveLoadState("loading")
|
||||
setLiveLoadError(null)
|
||||
Promise.all([
|
||||
apiFetch<BackendServer[]>("/api/servers"),
|
||||
apiFetch<LiveFiltersResponse>("/api/filters/rules"),
|
||||
apiFetch<unknown>("/api/servers"),
|
||||
apiFetch<unknown>("/api/filters/rules"),
|
||||
])
|
||||
.then(([backendServers, liveFilters]) => {
|
||||
.then(([serverPayload, filtersPayload]) => {
|
||||
if (cancelled) return
|
||||
const serverResult = firewallServersSchema.safeParse(serverPayload)
|
||||
const filtersResult = filtersResponseSchema.safeParse(filtersPayload)
|
||||
if (!serverResult.success || !filtersResult.success) throw new Error("Сервер вернул некорректные данные Filters")
|
||||
const backendServers = serverResult.data, liveFilters = filtersResult.data
|
||||
if (liveFilters.stale) throw new Error("Актуальные правила недоступны")
|
||||
if (backendServers.some(server => server.enabled && !liveFilters.rulesets.some(rs => rs.serverId === String(server.id)))) throw new Error("Сервер не вернул наборы правил всех устройств")
|
||||
const mappedServers: Server[] = backendServers.map(s => ({
|
||||
id: String(s.id),
|
||||
name: s.name || s.host,
|
||||
@@ -1416,21 +1409,23 @@ export default function FiltersPage() {
|
||||
))
|
||||
setLiveLoadState("idle")
|
||||
})
|
||||
.catch(() => {
|
||||
setLiveServers([])
|
||||
setGreByServer({})
|
||||
setRulesets(buildRulesets(servers, serverFilterRulesets))
|
||||
setSelectedServerId(servers[0]?.id ?? "")
|
||||
.catch(error => {
|
||||
if (cancelled) return
|
||||
setLiveLoadError(error instanceof Error ? error.message : "Не удалось загрузить Filters")
|
||||
setLiveLoadState("error")
|
||||
})
|
||||
}, [isLive, apiFetch])
|
||||
return () => { cancelled = true }
|
||||
}, [isLive, apiFetch, loadAttempt])
|
||||
|
||||
const loadLiveRules = useCallback(async (serverId: string) => {
|
||||
if (!isLive || !serverId) return false
|
||||
try {
|
||||
const fresh = await apiFetch<LiveFiltersResponse>(
|
||||
const payload = await apiFetch<unknown>(
|
||||
`/api/filters/rules?serverId=${encodeURIComponent(serverId)}`,
|
||||
)
|
||||
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("Не удалось получить актуальные правила выбранного устройства")
|
||||
const liveRules = ruleset.rules
|
||||
@@ -1440,9 +1435,6 @@ export default function FiltersPage() {
|
||||
return prev.map((rs) => rs.serverId === serverId ? { ...rs, rules: liveRules } : rs)
|
||||
})
|
||||
setLiveStale(Boolean(fresh.stale))
|
||||
if (fresh.greTunnels?.length) {
|
||||
setGreByServer((prev) => ({ ...prev, [serverId]: fresh.greTunnels }))
|
||||
}
|
||||
return true
|
||||
} catch (err) {
|
||||
setLiveStale(true)
|
||||
@@ -1461,8 +1453,8 @@ export default function FiltersPage() {
|
||||
])
|
||||
}, [isLive, selectedServerId, liveLoadState, liveServers, ensureGreTunnels, ensureRecursiveRoutes, loadLiveRules])
|
||||
|
||||
const allServers = !isLive || liveLoadState === "error" ? servers : liveServers
|
||||
const allTunnels = !isLive || liveLoadState === "error" ? greTunnels : (greByServer[selectedServerId] ?? [])
|
||||
const allServers = !isLive ? servers : liveServers
|
||||
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"))
|
||||
@@ -1525,11 +1517,13 @@ export default function FiltersPage() {
|
||||
applyLock.current = true
|
||||
setApplyBusy(true)
|
||||
try {
|
||||
const res = await apiFetch<{ ok: boolean; rules?: FilterRule[] }>("/api/filters/rules", {
|
||||
const payload = await apiFetch<unknown>("/api/filters/rules", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ serverId, rules: nextRules, source }),
|
||||
})
|
||||
if (res?.ok !== true) throw new Error("Сервер не подтвердил применение правил")
|
||||
const parsed = filtersMutationSchema.safeParse(payload)
|
||||
if (!parsed.success) throw new Error("Сервер не подтвердил применение правил")
|
||||
const res = parsed.data
|
||||
setRulesets(p => p.map(rs => rs.serverId === serverId ? { ...rs, rules: res.rules ?? nextRules } : rs))
|
||||
toast.success("Правила применены на роутер")
|
||||
return { ok: true }
|
||||
@@ -1587,11 +1581,13 @@ export default function FiltersPage() {
|
||||
setApplyBusy(true)
|
||||
setHistoryRestoring(true)
|
||||
try {
|
||||
const res = await apiFetch<{ ok: boolean; rules?: FilterRule[] }>(
|
||||
const payload = await apiFetch<unknown>(
|
||||
`/api/filters/revisions/${encodeURIComponent(id)}/restore`,
|
||||
{ method: "POST", body: JSON.stringify({ serverId: selectedServerId }) },
|
||||
)
|
||||
if (res?.ok !== true) throw new Error("Сервер не подтвердил восстановление")
|
||||
const parsed = filtersMutationSchema.safeParse(payload)
|
||||
if (!parsed.success) throw new Error("Сервер не подтвердил восстановление")
|
||||
const res = parsed.data
|
||||
if (res.rules) {
|
||||
setRulesets((p) => p.map((rs) => rs.serverId === selectedServerId ? { ...rs, rules: res.rules ?? [] } : rs))
|
||||
} else {
|
||||
@@ -1711,6 +1707,13 @@ export default function FiltersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (isLive && liveLoadState === "error" && liveServers.length === 0) {
|
||||
return <div className="flex h-full flex-col items-center justify-center gap-3 p-6">
|
||||
<p role="alert" className="text-sm text-destructive">Не удалось загрузить Filters: {liveLoadError}</p>
|
||||
<Button variant="outline" onClick={() => setLoadAttempt(n => n + 1)}>Повторить загрузку</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (isLive && liveLoadState === "loading") {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 text-sm text-muted-foreground">
|
||||
@@ -1792,7 +1795,8 @@ export default function FiltersPage() {
|
||||
{isLive && liveLoadState === "error" && (
|
||||
<div className="shrink-0 border-b border-destructive/30 bg-destructive/10 px-6 py-2.5 text-xs text-destructive flex items-center gap-2">
|
||||
<AlertCircleIcon className="size-3.5 shrink-0" />
|
||||
Бекенд недоступен — показаны демо-данные из lib/data. Проверьте URL бекенда в настройках.
|
||||
<span role="alert">Не удалось загрузить Filters: {liveLoadError}. Сохранены последние данные.</span>
|
||||
<Button variant="outline" size="sm" onClick={() => setLoadAttempt(n => n + 1)}>Повторить загрузку</Button>
|
||||
</div>
|
||||
)}
|
||||
{isLive && liveStale && liveLoadState !== "error" && (
|
||||
|
||||
Reference in new issue
Block a user