"use client"
import { useCallback, useState } from "react"
import Link from "next/link"
import { PageHeader } from "@/components/page-header"
import { useTheme } from "@/components/theme-provider"
import { FileImportDialog } from "@/components/file-import-dialog"
import { EvoBgpSettingsSection } from "@/components/settings/evobgp-settings-section"
import { OpsPanel } from "@/components/ops-panel"
import { Button, buttonVariants } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { useDataSource } from "@/lib/data-source"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import {
CheckIcon,
AlertCircleIcon,
LoaderCircleIcon,
DownloadIcon, UploadIcon, XIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { downloadSystemDatabaseBackup, restoreSystemDatabaseBackup } from "@/shared/api/system-database"
import { toast } from "sonner"
const SECTIONS_NAV = ["Общие", "EvoBGP", "Уведомления", "API-ключи", "Безопасность"] as const
type NavSection = typeof SECTIONS_NAV[number]
function SettingRow({ label, description, children }: { label: string; description?: string; children: React.ReactNode }) {
return (
{label}
{description &&
{description}
}
{children}
)
}
function DatabaseRestoreConfirm({
open,
filename,
busy,
onConfirm,
onCancel,
}: {
open: boolean
filename: string
busy?: boolean
onConfirm: () => void
onCancel: () => void
}) {
return (
{ if (!v && !busy) onCancel() }}>
Восстановить базу приложения?
Файл: {filename}. Текущие данные PostgreSQL на бекенде будут полностью заменены.
Рекомендуется сначала скачать актуальный бэкап.
Отмена
{busy ? : null}
{busy ? "Восстановление…" : "Восстановить"}
)
}
// ─── page ─────────────────────────────────────────────────────────────────────
export default function SettingsPage() {
const [section, setSection] = useState("Общие")
const { mode, setMode, backendUrl, backendUrlLocked, mockModeAvailable, backendStatus, checkBackend } = useDataSource()
const { theme, setTheme } = useTheme()
const [dbBackupBusy, setDbBackupBusy] = useState(false)
const [dbRestoreBusy, setDbRestoreBusy] = useState(false)
const [dbRestoreFile, setDbRestoreFile] = useState(null)
const [dbRestoreDialogOpen, setDbRestoreDialogOpen] = useState(false)
const systemDbAvailable = mode === "live" && backendStatus === true
const handleSystemDatabaseBackup = useCallback(async () => {
if (!systemDbAvailable) return
setDbBackupBusy(true)
try {
const { blob, filename } = await downloadSystemDatabaseBackup(backendUrl)
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
toast.success("Бэкап базы приложения скачан")
} catch (e) {
toast.error(e instanceof Error ? e.message : "Не удалось создать бэкап")
} finally {
setDbBackupBusy(false)
}
}, [backendUrl, systemDbAvailable])
const handleSystemDatabaseRestoreConfirm = useCallback(async () => {
if (!dbRestoreFile || !systemDbAvailable) return
setDbRestoreBusy(true)
try {
await restoreSystemDatabaseBackup(backendUrl, dbRestoreFile)
toast.success("База приложения восстановлена")
setDbRestoreFile(null)
} catch (e) {
toast.error(e instanceof Error ? e.message : "Не удалось восстановить базу")
} finally {
setDbRestoreBusy(false)
}
}, [backendUrl, dbRestoreFile, systemDbAvailable])
const renderContent = () => {
// ── Общие ──
if (section === "Общие") return (
{/* ── Источник данных ── */}
{/* mode toggle */}
{mockModeAvailable ? (
{([["mock", "Моковые"], ["live", "Живые"]] as const).map(([v, l], i) => (
setMode(v)}
className={cn(
"px-3 text-xs transition-colors border-input",
i === 0 && "border-r",
mode === v
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted",
)}>
{l}
))}
) : (
Живые
)}
{/* backend URL — visible in both modes so user can configure before switching */}
{/* connection status */}
{backendStatus === undefined && (
Не проверялось
)}
{backendStatus === true && (
Доступен
)}
{backendStatus === false && (
Недоступен
)}
Проверить
{/* hint when live but backend unreachable */}
{mode === "live" && backendStatus === false && (
Сервер недоступен. Последние полученные данные сохраняются; новые запросы могут завершаться ошибкой.
)}
Русский
{([["light","Светлая"],["dark","Тёмная"],["system","Авто"]] as const).map(([value,label]) => setTheme(value)}>{label} )}
Периоды сбора данных и параметры диагностики настраиваются в соответствующих разделах.
Сбор данных Диагностика
{!systemDbAvailable && (
Доступно только в live-режиме при доступном бекенде.
)}
{ void handleSystemDatabaseBackup() }}
>
{dbBackupBusy ? : }
{dbBackupBusy ? "Подготовка…" : "Скачать"}
setDbRestoreDialogOpen(true)}
>
Выбрать файл
{dbRestoreFile && (
{dbRestoreFile.name}
)}
{systemDbAvailable ? Открыть NetFlow : Открыть NetFlow }
Открыть оптимизатор
)
if (section === "EvoBGP") return
if (section === "Уведомления") return Открыть оповещения
if (section === "API-ключи") return Создание и управление отдельными API-ключами в приложении пока недоступны.
return Состояние MFA, ограничений IP и настроек аудита в этом разделе не получено. Изменение этих параметров здесь недоступно.
Управление пользователями
}
return (
{/* nav */}
{SECTIONS_NAV.map(s => (
setSection(s)}
className={cn(
"text-left px-3 py-2 rounded-md text-sm transition-colors",
s === section ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted text-muted-foreground hover:text-foreground",
)}>
{s}
))}
{/* content */}
{renderContent()}
{/* delete confirm */}
{dbRestoreFile && (
{ void handleSystemDatabaseRestoreConfirm() }}
onCancel={() => {
if (dbRestoreBusy) return
setDbRestoreFile(null)
}}
/>
)}
{
const file = files[0]
if (!file) return
setDbRestoreFile(file)
}}
/>
)
}
function BackendUrlField(){
const {backendUrl,setBackendUrl,backendUrlLocked}=useDataSource()
const [draft,setDraft]=useState(backendUrl),[error,setError]=useState(null)
function commit(){
if(backendUrlLocked||draft===backendUrl)return
try{const url=new URL(draft.trim());if(!["http:","https:"].includes(url.protocol)||url.username||url.password||url.search||url.hash)throw Error();setBackendUrl(url.toString().replace(/\/$/,""));setError(null)}
catch{setError("Укажите HTTP(S) адрес без учётных данных и параметров")}
}
return {setDraft(e.target.value);setError(null)}} onBlur={commit} onKeyDown={e=>{if(e.key==="Enter"){e.preventDefault();commit()}}} readOnly={backendUrlLocked} aria-invalid={!!error} placeholder="http://localhost:8000"/>{error?
{error}
:null}
}