Docker images / prepare-release (push) Successful in 7s
Docker images / backend-image (push) Successful in 1m26s
Docker images / frontend-image (push) Successful in 2m26s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 41s
Docker images / publish-release (push) Successful in 8s
Enhanced the error handling in the settings page by introducing a dedicated error message function. Added success and error toast notifications for better user feedback during settings save operations. Updated API key normalization to ensure consistent handling across the application.
223 lines
6.1 KiB
TypeScript
223 lines
6.1 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 { 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<EvoBgpCatalogSnapshot>(
|
|
backendUrl,
|
|
"/api/evobgp/catalog",
|
|
{ method: "POST" },
|
|
)
|
|
setSnapshot(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
|
|
}
|