"use client" import {useEffect,useRef,useState} from "react" import {evoBgpSettingsPatchSchema,evoBgpTestDraftSchema,type EvoBgpSavePayload} from "@mmapp/contracts/evobgp" import {useEvoBGP} from "@/lib/evobgp-context" import {useDataSource} from "@/lib/data-source" import {can} from "@/lib/auth" import {EvoBgpDataStatus} from "./evobgp-data-status" import {OpsPanel} from "@/components/ops-panel" import {FormField,FormToggle} from "@/components/form-kit" import {Alert,AlertDescription,AlertTitle} from "@/components/reui/alert" import {Button} from "@/components/ui/button" import {Input} from "@/components/ui/input" import {AlertDialog,AlertDialogContent,AlertDialogHeader,AlertDialogTitle,AlertDialogDescription,AlertDialogFooter,AlertDialogCancel,AlertDialogAction} from "@/components/ui/alert-dialog" import {AlertCircleIcon,EyeIcon,EyeOffIcon} from "lucide-react" export function EvoBgpSettingsSection(){const evo=useEvoBGP();return } function EvoBgpSettingsForm(){ const evo=useEvoBGP(),{mode,backendStatus}=useDataSource() const [baseUrl,setBaseUrl]=useState(evo.settingsLoaded?evo.baseUrl:"") const [enabled,setEnabled]=useState(evo.settingsLoaded?evo.enabled:false) const [apiKey,setApiKey]=useState("") const [showKey,setShowKey]=useState(false) const [changed,setChanged]=useState(false) const [busy,setBusy]=useState<"save"|"test"|"clear"|"refresh"|null>(null) const [error,setError]=useState(null) const [outcome,setOutcome]=useState(null) const [testResult,setTestResult]=useState<{ok:boolean;message:string}|null>(null) const [clearOpen,setClearOpen]=useState(false) const dirty=useRef(new Set<"baseUrl"|"enabled"|"apiKey">()) const active=useRef(true),lock=useRef(false) useEffect(()=>{active.current=true;return()=>{active.current=false}},[]) useEffect(()=>{if(!evo.settingsLoaded)return;let current=true;queueMicrotask(()=>{if(!current||!active.current)return;if(!dirty.current.has("baseUrl"))setBaseUrl(evo.baseUrl);if(!dirty.current.has("enabled"))setEnabled(evo.enabled)});return()=>{current=false}},[evo.settingsLoaded,evo.baseUrl,evo.enabled,evo.settingsReceivedAt]) const writable=mode==="live"&&backendStatus===true&&can("mm:settings:admin")&&evo.settingsLoaded&&!evo.settingsError const blocked=!!busy||evo.operationBusy const mark=(field:"baseUrl"|"enabled"|"apiKey")=>{dirty.current.add(field);setChanged(true);setOutcome(null);setTestResult(null)} function begin(kind:NonNullable){if(!active.current||lock.current||!writable||evo.operationBusy)return false;lock.current=true;setBusy(kind);setError(null);setOutcome(null);setTestResult(null);return true} function finish(){lock.current=false;if(active.current)setBusy(null)} async function save(){ if(!changed||!begin("save"))return try{ const patch:EvoBgpSavePayload={} if(dirty.current.has("baseUrl"))patch.baseUrl=baseUrl if(dirty.current.has("enabled"))patch.enabled=enabled if(dirty.current.has("apiKey")&&apiKey.trim())patch.apiKey=apiKey const parsed=evoBgpSettingsPatchSchema.safeParse(patch) if(!parsed.success)throw new Error(parsed.error.issues[0]?.message??"Проверьте поля EvoBGP") if(parsed.data.apiKey==="")throw new Error("Введите API-ключ; удаление сохранённого ключа подтверждается отдельно") await evo.saveSettings(parsed.data) if(!active.current)return for(const field of Object.keys(parsed.data))dirty.current.delete(field as "baseUrl"|"enabled"|"apiKey") if(parsed.data.baseUrl!==undefined)setBaseUrl(parsed.data.baseUrl) if(parsed.data.apiKey!==undefined){setApiKey("");setShowKey(false)} setChanged(dirty.current.size>0);setOutcome("Настройки EvoBGP сохранены") }catch(error){if(active.current)setError((error instanceof Error?error.message:"Не удалось сохранить EvoBGP")+". Черновик сохранён; автоматического повтора нет.")} finally{finish()} } async function test(){ if(!begin("test"))return try{ const draft=evoBgpTestDraftSchema.safeParse({baseUrl,...(apiKey.trim()?{apiKey}:{})}) if(!draft.success)throw new Error(draft.error.issues[0]?.message??"Проверьте поля EvoBGP") if(!draft.data.baseUrl||(!draft.data.apiKey&&!evo.secretConfigured))throw new Error("Укажите URL и API-ключ для проверки") const result=await evo.testConnection(draft.data) if(active.current)setTestResult(result) }catch(error){if(active.current)setError(error instanceof Error?error.message:"Не удалось проверить EvoBGP")} finally{finish()} } async function clear(){ if(!evo.secretConfigured||!begin("clear"))return try{await evo.saveSettings({apiKey:null});if(active.current){setClearOpen(false);setOutcome("Удаление сохранённого ключа подтверждено")}} catch(error){if(active.current)setError((error instanceof Error?error.message:"Удаление ключа не подтверждено")+". Подтверждения удаления нет; автоматического повтора нет.")} finally{finish()} } async function refresh(){if(!begin("refresh"))return;try{await evo.refresh()}finally{finish()}} return
{mode!=="live"?

Интеграция доступна в live-режиме. Выберите живые данные в разделе «Общие».

:null} {mark("baseUrl");setBaseUrl(e.target.value)}} disabled={!writable||blocked} className="min-w-0 font-mono" autoComplete="url"/>
{setApiKey(e.target.value);if(e.target.value.trim())mark("apiKey");else{dirty.current.delete("apiKey");setChanged(dirty.current.size>0)}}} placeholder={evo.secretConfigured?"Оставьте пустым, чтобы не менять":"Bearer-токен"} disabled={!writable||blocked} className="min-w-0 font-mono"/>
{mark("enabled");setEnabled(value)}}/>Использовать каталог EvoBGP{!evo.settingsLoaded?": —":""}
{error?:null} {outcome?

{outcome}

:null} {testResult?

{testResult.message}

:null}

Проверка соединения не сохраняет изменения. Пустое поле ключа не удаляет сохранённый ключ.

{if(!blocked)setClearOpen(value)}}>Удалить сохранённый ключ EvoBGP?Каталог не сможет обновляться до сохранения нового ключа. Остальные поля и введённый новый ключ останутся в форме.{error?

{error}

:null}Отмена{event.preventDefault();void clear()}}>{busy==="clear"?"Удаление…":"Удалить ключ"}
}