fix(ui): сохранять недоступные шлюзы Filters
This commit is contained in:
1 parent
860e88ed3e
commit
0ab5a23eb3
7 files changed
+149
-30
No files matched your search
+55
-26
@@ -14,7 +14,7 @@ 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 { filtersResponseSchema, filtersMutationSchema, filtersGreResponseSchema, filtersRecursiveResponseSchema } from "@/lib/filters-response"
|
||||
import { firewallServersSchema } from "@/lib/firewall-response"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import {
|
||||
@@ -818,6 +818,7 @@ function PreviewModal({ open, serverId, rulesets, onClose, serversList, tunnelsL
|
||||
// ── Copy Rules Sheet ──────────────────────────────────────────────────────────
|
||||
|
||||
type CopyMode = "append" | "replace"
|
||||
type GatewayReadiness = Record<string, Partial<Record<"gre" | "recursive", boolean>>>
|
||||
|
||||
/** Try to find an equivalent tunnel on the target server (same remoteAddress). */
|
||||
function mapTunnel(
|
||||
@@ -876,6 +877,7 @@ function CopyRulesSheet({
|
||||
ensureGreFor,
|
||||
recRoutesByServer,
|
||||
ensureRecursiveFor,
|
||||
catalogReady,
|
||||
}: {
|
||||
open: boolean; onClose: () => void
|
||||
sourceServerId: string; sourceRules: FilterRule[]
|
||||
@@ -886,6 +888,7 @@ function CopyRulesSheet({
|
||||
ensureGreFor?: (serverId: string) => void
|
||||
recRoutesByServer: Record<string, RecursiveRouteLite[]>
|
||||
ensureRecursiveFor?: (serverId: string) => void
|
||||
catalogReady?: GatewayReadiness
|
||||
}) {
|
||||
const [selected, setSelected] = useState<Set<string>>(() => new Set(sourceRules.map(r => r.id)))
|
||||
const [targetId, setTargetId] = useState<string | null>(null)
|
||||
@@ -972,7 +975,7 @@ function CopyRulesSheet({
|
||||
}
|
||||
|
||||
async function handleCopy() {
|
||||
if (copyLock.current || !targetId || selectedRules.length === 0) return
|
||||
if (copyLock.current || !targetId || selectedRules.length === 0 || !mappingReady) return
|
||||
copyLock.current = true
|
||||
setCopying(true)
|
||||
setCopyError(null)
|
||||
@@ -1010,7 +1013,18 @@ function CopyRulesSheet({
|
||||
}
|
||||
|
||||
const close = () => { if (!copyLock.current) onClose() }
|
||||
const canCopy = targetId !== null && selectedRules.length > 0 && !copying
|
||||
const requiredKinds = selectedRules.filter(r => (r.action ?? "route") === "route").map(r => isRecursiveGatewayRef(r.gatewayTunnelId) ? "recursive" as const : "gre" as const)
|
||||
const catalogsReady = !catalogReady || (targetId !== null && requiredKinds.every(kind => catalogReady[sourceServerId]?.[kind] && catalogReady[targetId]?.[kind]))
|
||||
const mappingReady = catalogsReady && selectedRules.every(rule => {
|
||||
if ((rule.action ?? "route") === "blackhole") return true
|
||||
if (!targetId) return false
|
||||
if (isRecursiveGatewayRef(rule.gatewayTunnelId)) {
|
||||
const source = sourceRecursiveRoutes.find(r => r.id === rule.gatewayTunnelId.slice(4))
|
||||
return !!source && !!pickRecursiveRouteForCopy(source, targetRecursiveRoutes)
|
||||
}
|
||||
return !!mapTunnel(rule.gatewayTunnelId, targetId, tunnelsList)
|
||||
})
|
||||
const canCopy = targetId !== null && selectedRules.length > 0 && !copying && mappingReady
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={v => { if (!v) close() }}>
|
||||
@@ -1218,7 +1232,7 @@ function CopyRulesSheet({
|
||||
))}
|
||||
{mapping.some(m => !m.mapped) && (
|
||||
<p className="text-[10px] text-muted-foreground/60">
|
||||
Правила с пустым шлюзом нужно отредактировать после копирования
|
||||
Копирование недоступно, пока для каждого маршрута не найден шлюз целевого устройства
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -1226,6 +1240,10 @@ function CopyRulesSheet({
|
||||
)}
|
||||
|
||||
</fieldset>
|
||||
{targetId && !mappingReady && <div className="px-5 py-3 flex flex-col gap-2">
|
||||
<p role="alert" className="text-sm text-destructive">Каталоги или сопоставление шлюзов не подтверждены. Копирование маршрутов заблокировано.</p>
|
||||
<Button variant="outline" disabled={copying} onClick={() => { for (const id of [sourceServerId, targetId]) { ensureGreFor?.(id); ensureRecursiveFor?.(id) } }}>Обновить шлюзы</Button>
|
||||
</div>}
|
||||
{copyError && <p role="alert" className="px-5 py-3 text-sm text-destructive">{copyError}</p>}
|
||||
{copying && <p role="status" className="px-5 py-2 text-sm text-muted-foreground">Ожидание подтверждения копирования…</p>}
|
||||
|
||||
@@ -1313,33 +1331,38 @@ function FiltersPage() {
|
||||
const [liveLoadError, setLiveLoadError] = useState<string | null>(null)
|
||||
const [loadAttempt, setLoadAttempt] = useState(0)
|
||||
|
||||
const ensureGreTunnels = useCallback(async (serverId: string) => {
|
||||
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 [gatewayReady, setGatewayReady] = useState<GatewayReadiness>({})
|
||||
const [gatewayErrors, setGatewayErrors] = useState<Record<string, Partial<Record<"gre" | "recursive", string>>>>({})
|
||||
const gatewayEpoch = useRef<Record<string, number>>({})
|
||||
|
||||
const ensureRecursiveRoutes = useCallback(async (serverId: string) => {
|
||||
const loadGatewayCatalog = useCallback(async (serverId: string, kind: "gre" | "recursive") => {
|
||||
if (!isLive || !mounted.current) return
|
||||
const key = serverId + ":" + kind
|
||||
const epoch = (gatewayEpoch.current[key] ?? 0) + 1
|
||||
gatewayEpoch.current[key] = epoch
|
||||
setGatewayReady(prev => ({ ...prev, [serverId]: { ...prev[serverId], [kind]: false } }))
|
||||
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]: [] }))
|
||||
const path = kind === "gre" ? "/api/filters/gre-tunnels" : "/api/recursive-routes"
|
||||
const payload = await apiFetch<unknown>(`${path}?serverId=${encodeURIComponent(serverId)}`)
|
||||
if (!mounted.current || gatewayEpoch.current[key] !== epoch) return
|
||||
if (kind === "gre") {
|
||||
const parsed = filtersGreResponseSchema.safeParse(payload)
|
||||
if (!parsed.success || parsed.data.tunnels.some(t => t.serverId !== serverId)) throw new Error("Некорректный каталог GRE")
|
||||
setGreByServer(prev => ({ ...prev, [serverId]: parsed.data.tunnels }))
|
||||
} else {
|
||||
const parsed = filtersRecursiveResponseSchema.safeParse(payload)
|
||||
if (!parsed.success || parsed.data.stale) throw new Error("Рекурсивные шлюзы недоступны или устарели")
|
||||
setRecRoutesByServer(prev => ({ ...prev, [serverId]: parsed.data.routes }))
|
||||
}
|
||||
setGatewayReady(prev => ({ ...prev, [serverId]: { ...prev[serverId], [kind]: true } }))
|
||||
setGatewayErrors(prev => ({ ...prev, [serverId]: { ...prev[serverId], [kind]: undefined } }))
|
||||
} catch (error) {
|
||||
if (!mounted.current || gatewayEpoch.current[key] !== epoch) return
|
||||
setGatewayErrors(prev => ({ ...prev, [serverId]: { ...prev[serverId], [kind]: error instanceof Error ? error.message : "Каталог шлюзов недоступен" } }))
|
||||
}
|
||||
}, [isLive, apiFetch])
|
||||
const ensureGreTunnels = useCallback((serverId: string) => loadGatewayCatalog(serverId, "gre"), [loadGatewayCatalog])
|
||||
const ensureRecursiveRoutes = useCallback((serverId: string) => loadGatewayCatalog(serverId, "recursive"), [loadGatewayCatalog])
|
||||
const [selectedServerId, setSelectedServerId] = useState("srv1")
|
||||
const [search, setSearch] = useState("")
|
||||
const [sheetOpen, setSheetOpen] = useState(false)
|
||||
@@ -1667,6 +1690,7 @@ function FiltersPage() {
|
||||
|
||||
const handleSave = async (form: RuleForm): Promise<string | null> => {
|
||||
if (mutationsLocked) return "Изменения заблокированы — обновите данные или дождитесь завершения операции"
|
||||
if (isLive && form.action === "route" && !gatewayReady[selectedServerId]?.[form.gatewayKind]) return "Шлюзы устройства не подтверждены — обновите шлюзы"
|
||||
const { gatewayKind: _gk, ...payload } = form
|
||||
let result
|
||||
if (sheetMode === "create") {
|
||||
@@ -1848,6 +1872,10 @@ function FiltersPage() {
|
||||
<Button variant="outline" size="sm" onClick={() => setLoadAttempt(n => n + 1)}>Повторить загрузку</Button>
|
||||
</div>
|
||||
)}
|
||||
{isLive && Object.values(gatewayErrors[selectedServerId] ?? {}).some(Boolean) && <div className="border-b px-6 py-3 flex items-center gap-3">
|
||||
<p role="alert" className="text-xs text-destructive">{Object.values(gatewayErrors[selectedServerId] ?? {}).filter(Boolean).join(" · ")}. Сохранены последние каталоги шлюзов.</p>
|
||||
<Button variant="outline" size="sm" disabled={applyBusy} onClick={() => { void ensureGreTunnels(selectedServerId); void ensureRecursiveRoutes(selectedServerId) }}>Обновить шлюзы</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">
|
||||
@@ -2058,6 +2086,7 @@ function FiltersPage() {
|
||||
ensureGreFor={ensureGreTunnels}
|
||||
recRoutesByServer={recRoutesByServer}
|
||||
ensureRecursiveFor={ensureRecursiveRoutes}
|
||||
catalogReady={isLive ? gatewayReady : undefined}
|
||||
/>
|
||||
|
||||
<ConfigHistorySheet
|
||||
|
||||
Reference in new issue
Block a user