224 lines
6.5 KiB
TypeScript
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
|
|
}
|