fix(ui): сохранять устройство оптимизатора в адресе
This commit is contained in:
1 parent
2cdfa2cb13
commit
c82434c435
5 files changed
+70
-19
No files matched your search
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState, useMemo, useRef } from "react"
|
||||
import { useCallback, useEffect, useState, useMemo, useRef, Suspense } from "react"
|
||||
import Link from "next/link"
|
||||
import {useSearchParams} from "next/navigation"
|
||||
import {can} from "@/lib/auth"
|
||||
import {ospfCatalogSchema,ospfOptimizeSchema} from "@/lib/ospf-response"
|
||||
import {optimizerServersSchema,optimizerRulesSchema,optimizerProbesSchema,optimizerOspfPreviewSchema,type OptimizerOspfPreview} from "@/lib/route-optimizer-response"
|
||||
@@ -396,11 +397,21 @@ function TopologyBar() {
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export default function RouteOptimizerPage() {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
return <RouteOptimizerContent key={JSON.stringify([mode, backendUrl])} />
|
||||
return <Suspense fallback={<p role="status">Загрузка контекста оптимизатора…</p>}><RouteOptimizerRoute /></Suspense>
|
||||
}
|
||||
function RouteOptimizerRoute() {
|
||||
const {mode,backendUrl}=useDataSource()
|
||||
const params=useSearchParams()
|
||||
const requestedServer=params.get("server")
|
||||
function selectServer(server:string) {
|
||||
const url=new URL(window.location.href)
|
||||
url.searchParams.set("server",server)
|
||||
window.history.pushState(null,"",url.pathname+url.search+url.hash)
|
||||
}
|
||||
return <RouteOptimizerContent key={JSON.stringify([mode,backendUrl])} requestedServer={requestedServer} onSelectServer={selectServer} />
|
||||
}
|
||||
|
||||
function RouteOptimizerContent() {
|
||||
function RouteOptimizerContent({requestedServer,onSelectServer}:{requestedServer:string|null;onSelectServer:(server:string)=>void}) {
|
||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
||||
/** Данные из API/БД при режиме «Живые»; не ждём /health — иначе до ответа показывались моки. */
|
||||
const useLiveData = mode === "live"
|
||||
@@ -449,7 +460,9 @@ function RouteOptimizerContent() {
|
||||
const [ecmpAlgo, setEcmpAlgo] = useState<"per-dst" | "per-conn" | "per-packet">("per-dst")
|
||||
const [rpfMode, setRpfMode] = useState<"disabled" | "loose" | "strict">("disabled")
|
||||
const [selectedVrf, setSelectedVrf] = useState("main")
|
||||
const [ospfServerId, setOspfServerId] = useState("")
|
||||
const ospfServerId=requestedServer ?? String(liveServers[0]?.id ?? "")
|
||||
const ospfEligible=/^[1-9]\d*$/.test(ospfServerId) && liveServers.some(server=>String(server.id)===ospfServerId)
|
||||
const contextError=requestedServer!==null && !loading && !ospfEligible ? "Устройство из ссылки неизвестно или отключено. Выберите доступное устройство." : ""
|
||||
const [ospfApplying, setOspfApplying] = useState(false)
|
||||
const [applyResult, setApplyResult] = useState<{owner:string;serverName:string;optimizedCount:number;receivedAt:string}|null>(null)
|
||||
const [applyError,setApplyError]=useState({owner:"",message:""})
|
||||
@@ -462,7 +475,7 @@ function RouteOptimizerContent() {
|
||||
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 ospfPreviewLoading=useLiveData && ospfEligible && (previewState.owner!==previewKey || previewState.pending)
|
||||
useEffect(()=>{activeContext.current=previewKey},[previewKey])
|
||||
const ospfApplyResult=applyResult?.owner===previewKey?applyResult:null
|
||||
const ospfApplyError=applyError.owner===previewKey?applyError.message:""
|
||||
@@ -502,7 +515,6 @@ function RouteOptimizerContent() {
|
||||
const {jumpHosts,exitNodes} = mapApiServersToTopology(rows)
|
||||
const enabledRows=rows.filter(row=>row.enabled)
|
||||
setLiveServers(enabledRows);setLiveJumpHosts(jumpHosts);setLiveExitNodes(exitNodes)
|
||||
setOspfServerId(previous=>previous && enabledRows.some(row=>String(row.id)===previous)?previous:String(enabledRows[0]?.id??""))
|
||||
setData(next);setError("");setReadAt(new Date().toISOString())
|
||||
} catch (error) {
|
||||
if (current()) setError(error instanceof Error ? error.message : "Ошибка загрузки данных")
|
||||
@@ -639,7 +651,7 @@ function RouteOptimizerContent() {
|
||||
setSettings(prev => ({ ...prev, [k]: v }))
|
||||
|
||||
useEffect(()=>{
|
||||
if(!useLiveData||!ospfServerId)return
|
||||
if(!useLiveData||!ospfEligible)return
|
||||
let cancelled=false
|
||||
void apiFetch<unknown>("/api/ospf/catalog").then(raw=>{
|
||||
if(cancelled)return
|
||||
@@ -655,10 +667,10 @@ function RouteOptimizerContent() {
|
||||
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])
|
||||
},[apiFetch,liveServers,ospfServerId,ospfEligible,useLiveData,previewTick])
|
||||
|
||||
useEffect(()=>{
|
||||
if(!useLiveData||!ospfServerId)return
|
||||
if(!useLiveData||!ospfEligible)return
|
||||
let cancelled=false
|
||||
queueMicrotask(()=>{
|
||||
if(cancelled)return
|
||||
@@ -673,7 +685,7 @@ function RouteOptimizerContent() {
|
||||
})
|
||||
})
|
||||
return()=>{cancelled=true}
|
||||
},[apiFetch,ospfServerId,settings.pingWeight,useLiveData,previewKey,readAt,previewTick])
|
||||
},[apiFetch,ospfServerId,ospfEligible,settings.pingWeight,useLiveData,previewKey,readAt,previewTick])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0 min-w-0">
|
||||
@@ -834,6 +846,7 @@ function RouteOptimizerContent() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{useLiveData && contextError && <p role="alert" className="text-xs text-destructive">{contextError}</p>}
|
||||
{useLiveData && (
|
||||
<>
|
||||
<div className="rounded-lg border bg-muted/15 overflow-hidden">
|
||||
@@ -841,9 +854,10 @@ function RouteOptimizerContent() {
|
||||
<select aria-label="Устройство OSPF"
|
||||
className="w-full min-w-0 sm:w-auto sm:min-w-64 text-xs bg-background text-foreground border border-input rounded-md px-2 py-1 h-7 focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
value={ospfServerId}
|
||||
onChange={(e) => setOspfServerId(e.target.value)}
|
||||
onChange={(e) => onSelectServer(e.target.value)}
|
||||
>
|
||||
{liveServers.length === 0 && <option value="">Нет доступных серверов</option>}
|
||||
{requestedServer !== null && !ospfEligible && <option value={requestedServer}>Недоступное устройство</option>}
|
||||
{liveServers.length === 0 && requestedServer === null && <option value="">Нет доступных серверов</option>}
|
||||
{liveServers.map((s) => (
|
||||
<option key={s.id} value={String(s.id)}>
|
||||
{s.name || s.host} ({s.host})
|
||||
@@ -851,13 +865,13 @@ function RouteOptimizerContent() {
|
||||
))}
|
||||
</select>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{ospfMetaError ? "Каталог OSPF недостоверен" : ospfMeta ? `${ospfMeta.interfaces} iface · ${ospfMeta.areas} area` : "сбор OSPF-метрик…"}
|
||||
{contextError ? "Устройство недоступно" : ospfMetaError ? "Каталог OSPF недостоверен" : ospfMeta ? `${ospfMeta.interfaces} iface · ${ospfMeta.areas} area` : "сбор OSPF-метрик…"}
|
||||
</span>
|
||||
<Button type="button"
|
||||
size="sm"
|
||||
className="ml-auto h-7 text-xs"
|
||||
onClick={() => void applyOspfOptimization()}
|
||||
disabled={!can("mm:network:write") || loading || Boolean(error) || !data || !ospfServerId || ospfApplying || !previewReady || Boolean(ospfMetaError) || !ospfPreview?.changedCount}
|
||||
disabled={!can("mm:network:write") || loading || Boolean(error) || !data || !ospfEligible || ospfApplying || !previewReady || Boolean(ospfMetaError) || !ospfPreview?.changedCount}
|
||||
>
|
||||
<ZapIcon aria-hidden="true" className={cn("size-3.5", ospfApplying && "animate-pulse")} />
|
||||
{ospfApplying ? "Оптимизация…" : "Оптимизировать OSPF"}
|
||||
@@ -876,7 +890,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 flex-wrap gap-2 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>
|
||||
<Button type="button" variant="outline" size="sm" disabled={!ospfEligible || ospfApplying || ospfPreviewLoading} onClick={()=>setPreviewTick(value=>value+1)}>Обновить preview OSPF</Button>
|
||||
<span>
|
||||
{ospfPreviewLoading
|
||||
? "расчёт…"
|
||||
|
||||
Reference in new issue
Block a user