fix(ui): подтверждать применение OSPF и согласовать права

This commit is contained in:
Denozordec committed 2026-09-30 22:56:31 +07:00
1 parent bf2815b5a4
commit c88e1400bf
8 files changed
+147 -35

No files matched your search

+37 -33
View File
@@ -2,7 +2,8 @@
import { useCallback, useEffect, useState, useMemo, useRef } from "react"
import Link from "next/link"
import {ospfCatalogSchema} from "@/lib/ospf-response"
import {can} from "@/lib/auth"
import {ospfCatalogSchema,ospfOptimizeSchema} 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"
@@ -450,8 +451,9 @@ function RouteOptimizerContent() {
const [selectedVrf, setSelectedVrf] = useState("main")
const [ospfServerId, setOspfServerId] = useState("")
const [ospfApplying, setOspfApplying] = useState(false)
const [ospfApplyResult, setOspfApplyResult] = useState<{ serverName: string; optimizedCount: number } | null>(null)
const [ospfApplyError, setOspfApplyError] = useState("")
const [applyResult, setApplyResult] = useState<{owner:string;serverName:string;optimizedCount:number;receivedAt:string}|null>(null)
const [applyError,setApplyError]=useState({owner:"",message:""})
const writeLock=useRef(false),activeContext=useRef("")
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)
@@ -461,6 +463,9 @@ function RouteOptimizerContent() {
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)
useEffect(()=>{activeContext.current=previewKey},[previewKey])
const ospfApplyResult=applyResult?.owner===previewKey?applyResult:null
const ospfApplyError=applyError.owner===previewKey?applyError.message:""
const previewReady=Boolean(ospfPreview) && !ospfPreviewLoading && !ospfPreviewError
const load = useCallback(async (override?: OptimizerSettings) => {
@@ -608,28 +613,25 @@ function RouteOptimizerContent() {
}
async function applyOspfOptimization() {
if (!ospfServerId || loading || error || !data || !previewReady) return
setOspfApplying(true)
setOspfApplyError("")
setOspfApplyResult(null)
if(writeLock.current || !can("mm:network:write") || !useLiveData || !ospfServerId || loading || error || !data || !previewReady || ospfMetaError || !ospfPreview?.changedCount || !liveServers.some(server=>String(server.id)===ospfServerId&&server.enabled))return
const owner=previewKey,target=ospfServerId,weight=settings.pingWeight
const current=()=>reader.current.alive && activeContext.current===owner
writeLock.current=true;setOspfApplying(true);setApplyError({owner,message:""})
try {
const res = await apiFetch<{ serverName: string; optimizedCount: number }>(
`/api/servers/${ospfServerId}/ospf/optimize`,
{
method: "POST",
body: JSON.stringify({ pingWeight: settings.pingWeight }),
},
)
setOspfApplyResult({
serverName: res.serverName,
optimizedCount: res.optimizedCount,
})
await load()
setPreviewTick(value=>value+1)
} catch (e) {
setOspfApplyError(e instanceof Error ? e.message : "Ошибка применения OSPF-оптимизации")
} finally {
setOspfApplying(false)
const raw=await apiFetch<unknown>("/api/servers/"+target+"/ospf/optimize",{method:"POST",body:JSON.stringify({pingWeight:weight})})
const parsed=ospfOptimizeSchema.safeParse(raw)
if(!parsed.success || String(parsed.data.serverId)!==target || parsed.data.pingWeight!==weight)throw new Error("Применение OSPF не подтверждено: недостоверный ответ или другой контекст")
if(current())setApplyResult({owner,serverName:parsed.data.serverName,optimizedCount:parsed.data.optimizedCount,receivedAt:new Date().toISOString()})
}catch(error){
if(current())setApplyError({owner,message:error instanceof Error?error.message:"Ошибка применения OSPF"})
}finally{
writeLock.current=false
if(reader.current.alive){
setOspfApplying(false)
setPreviewState(previous=>({...previous,pending:true}))
setPreviewTick(value=>value+1)
void load()
}
}
}
@@ -684,7 +686,7 @@ function RouteOptimizerContent() {
<ZapIcon className="size-4" />Симулировать все ({totalSwitches})
</Button>
)}
<Button variant="outline" size="sm" onClick={() => void load()} disabled={loading}>
<Button variant="outline" size="sm" onClick={() => void load()} disabled={loading || ospfApplying}>
<RefreshCwIcon className={cn("size-4", loading && "animate-spin")} />
{loading ? "Расчёт…" : "Обновить"}
</Button>
@@ -693,7 +695,7 @@ function RouteOptimizerContent() {
/>
<div className="flex-1 overflow-y-auto p-6">
<div className="flex flex-col gap-4">
<fieldset disabled={ospfApplying} className="min-w-0 flex flex-col gap-4">
{/* Status row */}
<div className="flex items-center gap-3 text-xs text-muted-foreground flex-wrap px-0.5">
@@ -748,7 +750,7 @@ function RouteOptimizerContent() {
{/* Settings */}
<Frame dense className="w-full">
<FramePanel className="p-0">
<button type="button" aria-expanded={showSettings} aria-label="Настройки оптимизатора"
<button type="button" aria-expanded={showSettings} aria-label="Настройки оптимизатора" disabled={ospfApplying}
className={cn("flex w-full items-center gap-3 px-4 py-3 text-left rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring", showSettings && "border-b")}
onClick={() => setShowSettings(v => !v)}
>
@@ -854,7 +856,7 @@ function RouteOptimizerContent() {
size="sm"
className="ml-auto h-7 text-xs"
onClick={() => void applyOspfOptimization()}
disabled={loading || Boolean(error) || !data || !ospfServerId || ospfApplying || !previewReady || Boolean(ospfMetaError)}
disabled={!can("mm:network:write") || loading || Boolean(error) || !data || !ospfServerId || ospfApplying || !previewReady || Boolean(ospfMetaError) || !ospfPreview?.changedCount}
>
<ZapIcon className={cn("size-3.5", ospfApplying && "animate-pulse")} />
{ospfApplying ? "Оптимизация…" : "Оптимизировать OSPF"}
@@ -865,7 +867,7 @@ function RouteOptimizerContent() {
<div className="text-muted-foreground">Router</div>
<div className="md:col-span-2 font-mono truncate">{ospfMeta?.serverName ?? "—"}</div>
<div className="text-right text-muted-foreground">
apply: {ospfApplyResult?.optimizedCount ?? 0}
apply: {ospfApplyResult?.optimizedCount ?? "—"}
</div>
</div>
</div>
@@ -889,14 +891,16 @@ function RouteOptimizerContent() {
/>
</div>
{ospfApplying && <p role="status" className="text-xs text-muted-foreground">Ожидание подтверждения OSPF API…</p>}
{!can("mm:network:write") && <p className="text-xs text-muted-foreground">Доступен просмотр OSPF. Для применения требуется право изменения сети.</p>}
{ospfApplyResult && (
<div className="text-xs rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-emerald-600 dark:text-emerald-400">
Применено на {ospfApplyResult.serverName}: изменено интерфейсов — {ospfApplyResult.optimizedCount}.
<div role="status" className="text-xs rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-emerald-600 dark:text-emerald-400">
Последнее подтверждение API: {ospfApplyResult.serverName}, изменено интерфейсов — {ospfApplyResult.optimizedCount}. Получено {new Date(ospfApplyResult.receivedAt).toLocaleTimeString("ru-RU")}.
</div>
)}
{ospfApplyError && (
<div className="text-xs rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-destructive">
<div role="alert" className="text-xs rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-destructive">
Ошибка OSPF-оптимизации: {ospfApplyError}
</div>
)}
@@ -1043,7 +1047,7 @@ function RouteOptimizerContent() {
/>
))}
</div>
</fieldset>
</div>
</div>
)