Files
2026-05-03 11:16:07 +07:00

224 lines
6.5 KiB
TypeScript

"use client"
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react"
import { useDataSource } from "@/lib/data-source"
import type { Domain, IpRange, Asn } from "@/lib/data"
export interface EvoBgpCommunityRow {
id: string
value: string
name: string
description: string
type: "standard" | "no-export" | "no-advertise" | "local-as" | "custom"
filterIds: string[]
serverCount: number
prefixCount: number
action: "permit" | "deny" | "local-pref" | "metric"
actionValue?: number
enabled: boolean
}
export interface EvoBgpCatalogSnapshot {
fetchedAt: string
modules: { id: string; name: string; type: string }[]
domains: Domain[]
ipRanges: IpRange[]
asns: Asn[]
communities: EvoBgpCommunityRow[]
}
export interface EvoBgpSettingsDto {
baseUrl: string
enabled: boolean
secretConfigured: boolean
}
/** undefined — не менять ключ; null или очистка через отдельное сохранение */
export type EvoBgpSavePayload = {
baseUrl?: string
enabled?: boolean
apiKey?: string | null
}
/** Черновик для POST /evobgp/test: пустой объект — всё из БД */
export type EvoBgpTestDraft = { baseUrl?: string; apiKey?: string }
interface EvoBgpContextValue {
baseUrl: string
enabled: boolean
secretConfigured: boolean
settingsLoaded: boolean
snapshot: EvoBgpCatalogSnapshot | null
loading: boolean
error: string | null
loadSettings: () => Promise<void>
saveSettings: (patch: EvoBgpSavePayload) => Promise<void>
refresh: () => Promise<void>
testConnection: (draft?: EvoBgpTestDraft) => Promise<{ ok: boolean; message: string }>
}
const EvoBgpContext = createContext<EvoBgpContextValue | null>(null)
export function EvoBGPProvider({ children }: { children: React.ReactNode }) {
const { mode, backendUrl, backendStatus } = useDataSource()
const [baseUrl, setBaseUrlState] = useState("")
const [enabled, setEnabledState] = useState(false)
const [secretConfigured, setSecretConfigured] = useState(false)
const [settingsLoaded, setSettingsLoaded] = useState(false)
const [snapshot, setSnapshot] = useState<EvoBgpCatalogSnapshot | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const pullCatalog = useCallback(
async (catalogEnabled: boolean) => {
if (mode !== "live" || backendStatus !== true || !catalogEnabled) {
setSnapshot(null)
setError(null)
return
}
setLoading(true)
setError(null)
try {
const res = await fetch(`${backendUrl.replace(/\/$/, "")}/api/evobgp/catalog`, {
method: "POST",
})
const text = await res.text()
if (!res.ok) {
let msg = res.statusText
try {
const j = JSON.parse(text) as { error?: string; detail?: string }
msg = j.error ?? j.detail ?? msg
} catch {
if (text) msg = text
}
throw new Error(msg || "Ошибка EvoBGP")
}
setSnapshot(JSON.parse(text) as EvoBgpCatalogSnapshot)
} catch (e) {
setSnapshot(null)
setError(e instanceof Error ? e.message : "Ошибка загрузки")
} finally {
setLoading(false)
}
},
[mode, backendStatus, backendUrl],
)
const loadSettings = useCallback(async () => {
if (mode !== "live" || backendStatus !== true) {
setSettingsLoaded(false)
setSnapshot(null)
setError(null)
return
}
try {
const res = await fetch(`${backendUrl.replace(/\/$/, "")}/api/evobgp/settings`)
if (!res.ok) throw new Error(await res.text())
const data = (await res.json()) as EvoBgpSettingsDto
setBaseUrlState(data.baseUrl ?? "")
setEnabledState(data.enabled ?? false)
setSecretConfigured(data.secretConfigured ?? false)
setSettingsLoaded(true)
await pullCatalog(data.enabled ?? false)
} catch {
setSettingsLoaded(true)
}
}, [mode, backendStatus, backendUrl, pullCatalog])
useEffect(() => {
queueMicrotask(() => {
void loadSettings()
})
}, [loadSettings])
const saveSettings = useCallback(
async (patch: EvoBgpSavePayload) => {
const res = await fetch(`${backendUrl.replace(/\/$/, "")}/api/evobgp/settings`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
})
const text = await res.text()
if (!res.ok) {
let msg = res.statusText
try {
const j = JSON.parse(text) as { error?: string }
msg = j.error ?? msg
} catch {
if (text) msg = text
}
throw new Error(msg || "Не удалось сохранить")
}
const data = JSON.parse(text) as EvoBgpSettingsDto
setBaseUrlState(data.baseUrl ?? "")
setEnabledState(data.enabled ?? false)
setSecretConfigured(data.secretConfigured ?? false)
await pullCatalog(data.enabled ?? false)
},
[backendUrl, pullCatalog],
)
const refresh = useCallback(async () => {
await pullCatalog(enabled)
}, [enabled, pullCatalog])
const testConnection = useCallback(async (draft?: EvoBgpTestDraft) => {
try {
const res = await fetch(`${backendUrl.replace(/\/$/, "")}/api/evobgp/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(draft ?? {}),
})
const data = (await res.json().catch(() => ({}))) as { ok?: boolean; error?: string }
if (!res.ok) throw new Error(data.error ?? res.statusText)
return { ok: true, message: "Соединение с EvoBGP установлено" }
} catch (e) {
return { ok: false, message: e instanceof Error ? e.message : "Ошибка" }
}
}, [backendUrl])
const value = useMemo(
() => ({
baseUrl,
enabled,
secretConfigured,
settingsLoaded,
snapshot,
loading,
error,
loadSettings,
saveSettings,
refresh,
testConnection,
}),
[
baseUrl,
enabled,
secretConfigured,
settingsLoaded,
snapshot,
loading,
error,
loadSettings,
saveSettings,
refresh,
testConnection,
],
)
return <EvoBgpContext.Provider value={value}>{children}</EvoBgpContext.Provider>
}
export function useEvoBGP(): EvoBgpContextValue {
const ctx = useContext(EvoBgpContext)
if (!ctx) throw new Error("useEvoBGP must be used within EvoBGPProvider")
return ctx
}