fix(ui): проверять контекст расчёта OSPF оптимизатора
This commit is contained in:
1 parent
f93dfe34c2
commit
bf2815b5a4
9 files changed
+131
-108
No files matched your search
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useCallback, useEffect, useState, useMemo, useRef } from "react"
|
||||
import Link from "next/link"
|
||||
import {optimizerServersSchema,optimizerRulesSchema,optimizerProbesSchema} from "@/lib/route-optimizer-response"
|
||||
import {ospfCatalogSchema} from "@/lib/ospf-response"
|
||||
import {optimizerServersSchema,optimizerRulesSchema,optimizerProbesSchema,optimizerOspfPreviewSchema,type OptimizerOspfPreview} from "@/lib/route-optimizer-response"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { RouteOptimizerWanMatrixDataGrid } from "@/components/data-grids/route-optimizer-wan-matrix-data-grid"
|
||||
import { RouteOptimizerFullRoutesDataGrid } from "@/components/data-grids/route-optimizer-full-routes-data-grid"
|
||||
@@ -451,30 +452,16 @@ function RouteOptimizerContent() {
|
||||
const [ospfApplying, setOspfApplying] = useState(false)
|
||||
const [ospfApplyResult, setOspfApplyResult] = useState<{ serverName: string; optimizedCount: number } | null>(null)
|
||||
const [ospfApplyError, setOspfApplyError] = useState("")
|
||||
const [ospfMeta, setOspfMeta] = useState<{ interfaces: number; areas: number; serverName: string } | null>(null)
|
||||
const [ospfPreviewError, setOspfPreviewError] = useState("")
|
||||
const [ospfPreview, setOspfPreview] = useState<{
|
||||
changedCount: number
|
||||
interfacesTotal: number
|
||||
interfaces: Array<{
|
||||
interface: string
|
||||
currentCost: number
|
||||
optimalCost: number
|
||||
score: number
|
||||
pingMs: number
|
||||
dlMbps: number
|
||||
ulMbps: number
|
||||
}>
|
||||
changes: Array<{
|
||||
interface: string
|
||||
currentCost: number
|
||||
optimalCost: number
|
||||
score: number
|
||||
pingMs: number
|
||||
dlMbps: number
|
||||
ulMbps: number
|
||||
}>
|
||||
} | null>(null)
|
||||
const [metaState,setMetaState]=useState<{owner:string;data:{interfaces:number;areas:number;serverName:string}|null;error:string}>({owner:"",data:null,error:""})
|
||||
const [previewState,setPreviewState]=useState<{owner:string;data:OptimizerOspfPreview|null;error:string;pending:boolean}>({owner:"",data:null,error:"",pending:false})
|
||||
const [previewTick,setPreviewTick]=useState(0)
|
||||
const previewKey=JSON.stringify([ospfServerId,settings.pingWeight])
|
||||
const ospfMeta=metaState.owner===ospfServerId?metaState.data:null
|
||||
const ospfMetaError=metaState.owner===ospfServerId?metaState.error:""
|
||||
const ospfPreview=previewState.owner===previewKey?previewState.data:null
|
||||
const ospfPreviewError=previewState.owner===previewKey?previewState.error:""
|
||||
const ospfPreviewLoading=useLiveData && Boolean(ospfServerId) && (previewState.owner!==previewKey || previewState.pending)
|
||||
const previewReady=Boolean(ospfPreview) && !ospfPreviewLoading && !ospfPreviewError
|
||||
|
||||
const load = useCallback(async (override?: OptimizerSettings) => {
|
||||
const epoch = ++reader.current.epoch
|
||||
@@ -621,7 +608,7 @@ function RouteOptimizerContent() {
|
||||
}
|
||||
|
||||
async function applyOspfOptimization() {
|
||||
if (!ospfServerId || loading || error || !data) return
|
||||
if (!ospfServerId || loading || error || !data || !previewReady) return
|
||||
setOspfApplying(true)
|
||||
setOspfApplyError("")
|
||||
setOspfApplyResult(null)
|
||||
@@ -638,7 +625,7 @@ function RouteOptimizerContent() {
|
||||
optimizedCount: res.optimizedCount,
|
||||
})
|
||||
await load()
|
||||
setOspfPreview((prev) => (prev ? { ...prev, changedCount: 0, changes: [] } : prev))
|
||||
setPreviewTick(value=>value+1)
|
||||
} catch (e) {
|
||||
setOspfApplyError(e instanceof Error ? e.message : "Ошибка применения OSPF-оптимизации")
|
||||
} finally {
|
||||
@@ -649,74 +636,42 @@ function RouteOptimizerContent() {
|
||||
const set = <K extends keyof OptimizerSettings>(k: K, v: OptimizerSettings[K]) =>
|
||||
setSettings(prev => ({ ...prev, [k]: v }))
|
||||
|
||||
useEffect(() => {
|
||||
if (!useLiveData || !ospfServerId) return
|
||||
let cancelled = false
|
||||
void apiFetch<{
|
||||
interfaces: Array<{ areaId: string; interface: string }>
|
||||
instances: Array<unknown>
|
||||
neighbors: Array<unknown>
|
||||
bfdSessions: Array<unknown>
|
||||
}>(`/api/servers/${ospfServerId}/ospf`)
|
||||
.then((data) => {
|
||||
if (cancelled) return
|
||||
const selected = liveServers.find((s) => String(s.id) === ospfServerId)
|
||||
const visibleInterfaces = data.interfaces.filter((i) => !/^\(ref\s+\*.+\)$/.test(i.interface.trim()))
|
||||
setOspfMeta({
|
||||
interfaces: visibleInterfaces.length,
|
||||
areas: new Set(visibleInterfaces.map((i) => i.areaId)).size,
|
||||
serverName: selected?.name || selected?.host || ospfServerId,
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
setOspfMeta(null)
|
||||
})
|
||||
return () => { cancelled = true }
|
||||
}, [apiFetch, liveServers, ospfServerId, useLiveData])
|
||||
useEffect(()=>{
|
||||
if(!useLiveData||!ospfServerId)return
|
||||
let cancelled=false
|
||||
void apiFetch<unknown>("/api/ospf/catalog").then(raw=>{
|
||||
if(cancelled)return
|
||||
const parsed=ospfCatalogSchema.safeParse(raw)
|
||||
if(!parsed.success)throw new Error("Недостоверный каталог OSPF")
|
||||
const catalog=parsed.data
|
||||
const failure=catalog.failures.find(item=>String(item.serverId)===ospfServerId)
|
||||
if(failure)throw new Error(failure.error)
|
||||
const interfaces=catalog.interfaces.filter(item=>String(item.serverId)===ospfServerId && !/^\(ref\s+\*.+\)$/.test(item.interface.trim()))
|
||||
const selected=liveServers.find(item=>String(item.id)===ospfServerId)
|
||||
setMetaState({owner:ospfServerId,data:{interfaces:interfaces.length,areas:new Set(interfaces.map(item=>item.areaId)).size,serverName:selected?.name||selected?.host||ospfServerId},error:""})
|
||||
}).catch(error=>{
|
||||
if(!cancelled)setMetaState(previous=>({owner:ospfServerId,data:previous.owner===ospfServerId?previous.data:null,error:error instanceof Error?error.message:"Ошибка каталога OSPF"}))
|
||||
})
|
||||
return()=>{cancelled=true}
|
||||
},[apiFetch,liveServers,ospfServerId,useLiveData,previewTick])
|
||||
|
||||
useEffect(() => {
|
||||
if (!useLiveData || !ospfServerId) return
|
||||
let cancelled = false
|
||||
void apiFetch<{
|
||||
changedCount: number
|
||||
interfacesTotal: number
|
||||
interfaces: Array<{
|
||||
interface: string
|
||||
currentCost: number
|
||||
optimalCost: number
|
||||
score: number
|
||||
pingMs: number
|
||||
dlMbps: number
|
||||
ulMbps: number
|
||||
}>
|
||||
changes: Array<{
|
||||
interface: string
|
||||
currentCost: number
|
||||
optimalCost: number
|
||||
score: number
|
||||
pingMs: number
|
||||
dlMbps: number
|
||||
ulMbps: number
|
||||
}>
|
||||
}>(
|
||||
`/api/servers/${ospfServerId}/ospf/optimize/preview`,
|
||||
{ method: "POST", body: JSON.stringify({ pingWeight: settings.pingWeight }) },
|
||||
)
|
||||
.then((data) => {
|
||||
if (cancelled) return
|
||||
setOspfPreviewError("")
|
||||
setOspfPreview(data)
|
||||
useEffect(()=>{
|
||||
if(!useLiveData||!ospfServerId)return
|
||||
let cancelled=false
|
||||
queueMicrotask(()=>{
|
||||
if(cancelled)return
|
||||
setPreviewState(previous=>({owner:previewKey,data:previous.owner===previewKey?previous.data:null,error:"",pending:true}))
|
||||
void apiFetch<unknown>("/api/servers/"+ospfServerId+"/ospf/optimize/preview",{method:"POST",body:JSON.stringify({pingWeight:settings.pingWeight})}).then(raw=>{
|
||||
if(cancelled)return
|
||||
const preview=optimizerOspfPreviewSchema.safeParse(raw)
|
||||
if(!preview.success||String(preview.data.serverId)!==ospfServerId||preview.data.pingWeight!==settings.pingWeight)throw new Error("Недостоверный preview OSPF или другой контекст")
|
||||
setPreviewState({owner:previewKey,data:preview.data,error:"",pending:false})
|
||||
}).catch(error=>{
|
||||
if(!cancelled)setPreviewState(previous=>({owner:previewKey,data:previous.owner===previewKey?previous.data:null,error:error instanceof Error?error.message:"Ошибка preview OSPF",pending:false}))
|
||||
})
|
||||
.catch((e) => {
|
||||
if (cancelled) return
|
||||
setOspfPreview(null)
|
||||
setOspfPreviewError(e instanceof Error ? e.message : "Ошибка preview OSPF")
|
||||
})
|
||||
return () => { cancelled = true }
|
||||
}, [apiFetch, ospfServerId, settings.pingWeight, useLiveData])
|
||||
|
||||
const ospfPreviewLoading = useLiveData && Boolean(ospfServerId) && !ospfPreview && !ospfPreviewError
|
||||
})
|
||||
return()=>{cancelled=true}
|
||||
},[apiFetch,ospfServerId,settings.pingWeight,useLiveData,previewKey,readAt,previewTick])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -862,6 +817,7 @@ function RouteOptimizerContent() {
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
|
||||
{ospfMetaError && <p role="alert" className="text-sm text-warning">Каталог OSPF: {ospfMetaError}. Актуальность неизвестна.</p>}
|
||||
{/* OSPF optimization from Route Optimizer */}
|
||||
<OpsPanel
|
||||
title="OSPF"
|
||||
@@ -892,13 +848,13 @@ function RouteOptimizerContent() {
|
||||
))}
|
||||
</select>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{ospfMeta ? `${ospfMeta.interfaces} iface · ${ospfMeta.areas} area` : "сбор OSPF-метрик…"}
|
||||
{ospfMetaError ? "Каталог OSPF недостоверен" : ospfMeta ? `${ospfMeta.interfaces} iface · ${ospfMeta.areas} area` : "сбор OSPF-метрик…"}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
className="ml-auto h-7 text-xs"
|
||||
onClick={() => void applyOspfOptimization()}
|
||||
disabled={loading || Boolean(error) || !data || !ospfServerId || ospfApplying}
|
||||
disabled={loading || Boolean(error) || !data || !ospfServerId || ospfApplying || !previewReady || Boolean(ospfMetaError)}
|
||||
>
|
||||
<ZapIcon className={cn("size-3.5", ospfApplying && "animate-pulse")} />
|
||||
{ospfApplying ? "Оптимизация…" : "Оптимизировать OSPF"}
|
||||
@@ -917,6 +873,7 @@ function RouteOptimizerContent() {
|
||||
<div className="rounded-lg border bg-background/70 overflow-hidden">
|
||||
<div className="px-3 py-2 border-b text-[11px] text-muted-foreground flex items-center justify-between">
|
||||
<span>Preview изменений OSPF cost (до применения)</span>
|
||||
<Button type="button" variant="outline" size="sm" disabled={ospfApplying || ospfPreviewLoading} onClick={()=>setPreviewTick(value=>value+1)}>Обновить preview OSPF</Button>
|
||||
<span>
|
||||
{ospfPreviewLoading
|
||||
? "расчёт…"
|
||||
@@ -926,10 +883,7 @@ function RouteOptimizerContent() {
|
||||
</span>
|
||||
</div>
|
||||
<RouteOptimizerOspfPreviewDataGrid
|
||||
rows={(ospfPreview?.interfaces ?? []).map((row) => ({
|
||||
id: `${row.interface}-${row.currentCost}-${row.optimalCost}`,
|
||||
...row,
|
||||
}))}
|
||||
rows={ospfPreview?.interfaces ?? []}
|
||||
error={ospfPreviewError || null}
|
||||
loading={ospfPreviewLoading}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user