'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; import { ClipboardList, Eye, RefreshCw, Search, 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, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { type ApplyLogItem, ApplyLogService } from '@/lib/services/openflare'; import { formatDateTime } from '@/lib/utils'; import { LogDetailSheet } from './components/log-detail-sheet'; const PAGE_SIZE_OPTIONS = [20, 50, 100]; function truncateHash(value: string) { if (!value) return '—'; return value.length > 12 ? `${value.slice(0, 12)}...` : value; } function ResultBadge({ result }: { result: string }) { const t = useTranslations('applyLogs'); if (result === 'success') { return ( {t('success')} ); } if (result === 'warning') { return ( {t('warning')} ); } return ( {t('failed')} ); } export function ApplyLogsPageClient() { const t = useTranslations('applyLogs'); const tc = useTranslations('common'); const searchParams = useSearchParams(); const initialNodeId = searchParams.get('node_id')?.trim() ?? ''; const [nodeFilterInput, setNodeFilterInput] = useState(initialNodeId); const [nodeFilter, setNodeFilter] = useState(initialNodeId); const [pageNo, setPageNo] = useState(1); const [pageSize, setPageSize] = useState(20); const [rows, setRows] = useState([]); const [current, setCurrent] = useState(1); const [total, setTotal] = useState(0); const [totalPage, setTotalPage] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedLog, setSelectedLog] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const [cleanupOpen, setCleanupOpen] = useState(false); const [cleaning, setCleaning] = useState(false); const summary = useMemo(() => { const nodeIds = new Set(rows.map((item) => item.node_id)); return [ { label: t('totalRecords'), value: total }, { label: t('currentPage'), value: current }, { label: t('totalPages'), value: totalPage }, { label: t('pageNodes'), value: nodeIds.size }, ]; }, [rows, total, current, totalPage, t]); const fetchLogs = useCallback(async () => { setLoading(true); setError(null); try { const data = await ApplyLogService.list({ node_id: nodeFilter || undefined, pageNo, pageSize, }); setRows(data.rows); setCurrent(data.current); setTotal(data.total); setTotalPage(data.totalPage); } catch (err) { setError(err instanceof Error ? err.message : t('loadFailed')); } finally { setLoading(false); } }, [nodeFilter, pageNo, pageSize, t]); useEffect(() => { void fetchLogs(); }, [fetchLogs]); useEffect(() => { const nodeId = searchParams.get('node_id')?.trim(); if (!nodeId) return; setNodeFilterInput(nodeId); setNodeFilter(nodeId); setPageNo(1); }, [searchParams]); const handleSearch = () => { setPageNo(1); setNodeFilter(nodeFilterInput.trim()); }; const handleResetFilters = () => { setNodeFilterInput(''); setNodeFilter(''); setPageNo(1); }; const handleCleanupLogs = async () => { setCleaning(true); try { const result = await ApplyLogService.cleanup({ delete_all: true }); toast.success(t('cleared'), { description: t('clearedDesc', { count: result.deleted_count }), }); setCleanupOpen(false); setPageNo(1); await fetchLogs(); } catch (err) { toast.error(t('clearFailed'), { description: err instanceof Error ? err.message : t('unknownError'), }); } finally { setCleaning(false); } }; return (

{t('title')}

{t('description')}

{summary.map((item) => (

{item.label}

{item.value}

))}

{t('nodeId')}

setNodeFilterInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSearch(); }} placeholder={t('nodePlaceholder')} className='pl-8 h-9 text-xs' />

{t('pageSize')}

{error ? ( void fetchLogs()} /> ) : null}
{loading ? ( ) : rows.length === 0 ? ( ) : ( {t('colNodeId')} {t('colVersion')} {t('colResult')} {t('colChecksum')} {t('colTime')} {t('colMessage')} {t('colActions')} {rows.map((log) => ( {log.node_id} {log.version} {truncateHash(log.checksum)} {formatDateTime(log.created_at)}
{log.message || '—'}
))}
)}
{!loading && rows.length > 0 ? (

{t('pageInfo', { current, total: Math.max(totalPage, 1), count: total, })}

) : null} {t('clearTitle')} {t('clearDesc', { count: total })} {tc('cancel')} { event.preventDefault(); void handleCleanupLogs(); }} > {cleaning ? t('clearing') : t('confirmClear')}
); }