fix(ui): подтверждать применение OSPF и согласовать права
This commit is contained in:
1 parent
bf2815b5a4
commit
c88e1400bf
8 files changed
+147
-35
No files matched your search
@@ -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>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user