'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.added_sites.length > 0 || diff.removed_sites.length > 0 || diff.modified_sites.length > 0 || diff.main_config_changed || diff.waf_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} 设为当前激活版本吗?` : ''} 取消
); }