Files
Denozordec 25e040a5dd
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
fix(settings): improve error handling and success notifications in EvoBGP settings
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.
2026-09-05 00:07:58 +07:00

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
}