224 lines
6.2 KiB
TypeScript
224 lines
6.2 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"
|
|
import { parseEvoBgpCatalog } from "@/lib/evobgp-catalog"
|
|
import { ApiClientError, requestJson } from "@/shared/api/http-client"
|
|
|
|
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)
|
|
|
|
function errorMessage(e: unknown, fallback: string): string {
|
|
if (e instanceof ApiClientError) return e.message || fallback
|
|
if (e instanceof Error) return e.message || fallback
|
|
return fallback
|
|
}
|
|
|
|
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 data = await requestJson<unknown>(
|
|
backendUrl,
|
|
"/api/evobgp/catalog",
|
|
{ method: "POST" },
|
|
)
|
|
setSnapshot(parseEvoBgpCatalog(data))
|
|
} catch (e) {
|
|
setSnapshot(null)
|
|
setError(errorMessage(e, "Ошибка загрузки"))
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
},
|
|
[mode, backendStatus, backendUrl],
|
|
)
|
|
|
|
const loadSettings = useCallback(async () => {
|
|
if (mode !== "live" || backendStatus !== true) {
|
|
setSettingsLoaded(false)
|
|
setSnapshot(null)
|
|
setError(null)
|
|
return
|
|
}
|
|
try {
|
|
const data = await requestJson<EvoBgpSettingsDto>(
|
|
backendUrl,
|
|
"/api/evobgp/settings",
|
|
)
|
|
setBaseUrlState(data.baseUrl ?? "")
|
|
setEnabledState(Boolean(data.enabled))
|
|
setSecretConfigured(Boolean(data.secretConfigured))
|
|
setSettingsLoaded(true)
|
|
setError(null)
|
|
await pullCatalog(Boolean(data.enabled))
|
|
} catch (e) {
|
|
setSettingsLoaded(true)
|
|
setError(errorMessage(e, "Не удалось загрузить настройки EvoBGP"))
|
|
}
|
|
}, [mode, backendStatus, backendUrl, pullCatalog])
|
|
|
|
useEffect(() => {
|
|
queueMicrotask(() => {
|
|
void loadSettings()
|
|
})
|
|
}, [loadSettings])
|
|
|
|
const saveSettings = useCallback(
|
|
async (patch: EvoBgpSavePayload) => {
|
|
const data = await requestJson<EvoBgpSettingsDto>(
|
|
backendUrl,
|
|
"/api/evobgp/settings",
|
|
{
|
|
method: "PUT",
|
|
body: JSON.stringify(patch),
|
|
},
|
|
)
|
|
const nextEnabled = Boolean(data.enabled)
|
|
setBaseUrlState(data.baseUrl ?? "")
|
|
setEnabledState(nextEnabled)
|
|
setSecretConfigured(Boolean(data.secretConfigured))
|
|
setError(null)
|
|
// Каталог не должен ронять успех сохранения (401/502 на catalog ≠ «настройки не сохранились»)
|
|
try {
|
|
await pullCatalog(nextEnabled)
|
|
} catch {
|
|
/* pullCatalog already sets error state */
|
|
}
|
|
},
|
|
[backendUrl, pullCatalog],
|
|
)
|
|
|
|
const refresh = useCallback(async () => {
|
|
await pullCatalog(enabled)
|
|
}, [enabled, pullCatalog])
|
|
|
|
const testConnection = useCallback(
|
|
async (draft?: EvoBgpTestDraft) => {
|
|
try {
|
|
await requestJson<{ ok?: boolean }>(backendUrl, "/api/evobgp/test", {
|
|
method: "POST",
|
|
body: JSON.stringify(draft ?? {}),
|
|
})
|
|
return { ok: true, message: "Соединение с EvoBGP установлено" }
|
|
} catch (e) {
|
|
return { ok: false, message: errorMessage(e, "Ошибка") }
|
|
}
|
|
},
|
|
[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
|
|
}
|