'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Eye, GitCompare, History, Loader2, Play, RefreshCw, Trash2, } from 'lucide-react'; import { useTranslations } from 'next-intl'; 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 t = useTranslations('configVersions'); const tc = useTranslations('common'); 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 : t('loadFailed')); } finally { setLoading(false); } }, [t]); 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 : t('previewLoadFailed'); 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 : t('diffLoadFailed')); } finally { setDiffLoading(false); } } }; const handlePublish = async (force = false) => { setPublishing(true); try { const version = await ConfigVersionService.publish(force); toast.success(t('publishSuccess'), { description: t('publishSuccessDesc', { version: version.version }), }); setPreviewOpen(false); setPublishConfirmOpen(false); setForcePublishConfirmOpen(false); setPreview(null); setDiff(null); await fetchVersions(); } catch (err) { toast.error(t('publishFailed'), { description: err instanceof Error ? err.message : t('unknownError'), }); } finally { setPublishing(false); } }; const handleActivate = async () => { if (!activateTarget) return; setActivating(true); try { const version = await ConfigVersionService.activate(activateTarget.id); toast.success(t('activateSuccess'), { description: t('activateSuccessDesc', { version: version.version }), }); setActivateTarget(null); await fetchVersions(); } catch (err) { toast.error(t('activateFailed'), { description: err instanceof Error ? err.message : t('unknownError'), }); } finally { setActivating(false); } }; const handleCleanup = async (keepCount: number) => { setCleaning(true); try { const result = await ConfigVersionService.cleanup({ keep_count: keepCount, }); toast.success(t('cleanupDone'), { description: t('cleanupDoneDesc', { count: result.deleted_count }), }); setCleanupOpen(false); await fetchVersions(); } catch (err) { toast.error(t('cleanupFailed'), { description: err instanceof Error ? err.message : t('unknownError'), }); } finally { setCleaning(false); } }; return (

{t('title')}

{t('description')}

{error ? ( void fetchVersions()} /> ) : null}
{loading ? ( ) : sortedVersions.length === 0 ? ( ) : ( {t('colVersion')} {t('colStatus')} {t('colAuthor')} {t('colChecksum')} {t('colCreatedAt')} {t('colActions')} {sortedVersions.map((version) => ( {version.version} {version.is_active ? ( {t('active')} ) : ( {t('history')} )} {version.created_by || t('system')} {truncateChecksum(version.checksum)} {formatDateTime(version.created_at)}
{!version.is_active ? ( ) : null}
))}
)}
setPublishConfirmOpen(true)} /> void handleCleanup(keepCount)} loading={cleaning} /> {t('confirmPublishTitle')} {t('confirmPublishDesc')} {tc('cancel')} {t('confirmForceTitle')} {t('confirmForceDesc')} {tc('cancel')} { if (!open) setActivateTarget(null); }} > {t('confirmActivateTitle')} {activateTarget ? t('confirmActivateDesc', { version: activateTarget.version }) : ''} {tc('cancel')}
); }