"use client" import {useCallback, useEffect, useMemo, useState} from "react" import {Eye, GitCompare, History, Loader2, Play, RefreshCw, Trash2,} from "lucide-react" import {toast} from "sonner" import {EmptyStateWithBorder} from "@/components/layout/empty" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import {Badge} from "@/components/ui/badge" import {Button} from "@/components/ui/button" import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table" import { type ConfigDiffResult, type ConfigPreviewResult, ConfigVersionService, type ConfigVersionSummary, } from "@/lib/services/openflare" import {formatDateTime} from "@/lib/utils" import {CleanupDialog} from "./components/cleanup-dialog" import {DiffDialog} from "./components/diff-dialog" import {PreviewSheet} from "./components/preview-sheet" import {VersionSnapshotSheet} from "./components/version-snapshot-sheet" function truncateChecksum(checksum: string) { if (!checksum) return "—" return checksum.length > 16 ? `${checksum.slice(0, 16)}...` : checksum } function hasConfigDiff(diff: ConfigDiffResult) { return ( diff.added_domains.length > 0 || diff.removed_domains.length > 0 || diff.modified_domains.length > 0 || diff.main_config_changed || diff.changed_option_keys.length > 0 || !diff.active_version ) } export default function ConfigVersionsPage() { const [versions, setVersions] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [previewOpen, setPreviewOpen] = useState(false) const [preview, setPreview] = useState(null) const [previewLoading, setPreviewLoading] = useState(false) const [previewError, setPreviewError] = useState(null) const [diffOpen, setDiffOpen] = useState(false) const [diff, setDiff] = useState(null) const [diffLoading, setDiffLoading] = useState(false) const [diffError, setDiffError] = useState(null) const [snapshotVersion, setSnapshotVersion] = useState(null) const [snapshotOpen, setSnapshotOpen] = useState(false) const [publishConfirmOpen, setPublishConfirmOpen] = useState(false) const [forcePublishConfirmOpen, setForcePublishConfirmOpen] = useState(false) const [activateTarget, setActivateTarget] = useState(null) const [cleanupOpen, setCleanupOpen] = useState(false) const [publishing, setPublishing] = useState(false) const [activating, setActivating] = useState(false) const [cleaning, setCleaning] = useState(false) const canPublish = useMemo( () => Boolean(preview && preview.route_count > 0 && diff && hasConfigDiff(diff)), [preview, diff], ) const sortedVersions = useMemo( () => [...versions].sort( (left, right) => new Date(right.created_at).getTime() - new Date(left.created_at).getTime(), ), [versions], ) const fetchVersions = useCallback(async () => { setLoading(true) setError(null) try { const data = await ConfigVersionService.list() setVersions(data) } catch (err) { setError(err instanceof Error ? err.message : "加载失败") } finally { setLoading(false) } }, []) useEffect(() => { void fetchVersions() }, [fetchVersions]) const loadPreviewData = async () => { setPreviewLoading(true) setPreviewError(null) setDiffLoading(true) setDiffError(null) try { const [previewData, diffData] = await Promise.all([ ConfigVersionService.preview(), ConfigVersionService.diff(), ]) setPreview(previewData) setDiff(diffData) } catch (err) { const message = err instanceof Error ? err.message : "加载预览失败" setPreviewError(message) setDiffError(message) } finally { setPreviewLoading(false) setDiffLoading(false) } } const handleOpenPreview = async () => { setPreviewOpen(true) if (!preview) { await loadPreviewData() } } const handleOpenDiff = async () => { setDiffOpen(true) if (!diff) { setDiffLoading(true) setDiffError(null) try { const diffData = await ConfigVersionService.diff() setDiff(diffData) } catch (err) { setDiffError(err instanceof Error ? err.message : "加载差异失败") } finally { setDiffLoading(false) } } } const handlePublish = async (force = false) => { setPublishing(true) try { const version = await ConfigVersionService.publish(force) toast.success("发布成功", { description: `版本 ${version.version}` }) setPreviewOpen(false) setPublishConfirmOpen(false) setForcePublishConfirmOpen(false) setPreview(null) setDiff(null) await fetchVersions() } catch (err) { toast.error("发布失败", { description: err instanceof Error ? err.message : "未知错误", }) } finally { setPublishing(false) } } const handleActivate = async () => { if (!activateTarget) return setActivating(true) try { const version = await ConfigVersionService.activate(activateTarget.id) toast.success("激活成功", { description: `版本 ${version.version}` }) setActivateTarget(null) await fetchVersions() } catch (err) { toast.error("激活失败", { description: err instanceof Error ? err.message : "未知错误", }) } finally { setActivating(false) } } const handleCleanup = async (keepCount: number) => { setCleaning(true) try { const result = await ConfigVersionService.cleanup({ keep_count: keepCount }) toast.success("清理完成", { description: `已删除 ${result.deleted_count} 个历史快照`, }) setCleanupOpen(false) await fetchVersions() } catch (err) { toast.error("清理失败", { description: err instanceof Error ? err.message : "未知错误", }) } finally { setCleaning(false) } } return (

配置版本

查看历史快照、预览待发布配置差异,并在需要时重新激活旧版本。

{error ? void fetchVersions()} /> : null}
{loading ? ( ) : sortedVersions.length === 0 ? ( ) : ( 版本号 状态 创建人 Checksum 创建时间 操作 {sortedVersions.map((version) => ( {version.version} {version.is_active ? ( 当前激活 ) : ( 历史版本 )} {version.created_by || "系统"} {truncateChecksum(version.checksum)} {formatDateTime(version.created_at)}
{!version.is_active ? ( ) : null}
))}
)}
setPublishConfirmOpen(true)} /> void handleCleanup(keepCount)} loading={cleaning} /> 确认发布配置 将把当前待发布配置生成新版本并设为激活版本,节点将随后拉取更新。 取消 确认强制发布 将忽略配置变化检查并立即生成一个新版本,请确认你确实需要重新发布。 取消 { if (!open) setActivateTarget(null) }} > 确认激活版本 {activateTarget ? `确认将版本 ${activateTarget.version} 设为当前激活版本吗?` : ""} 取消
) }