'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')} void fetchLogs()} disabled={loading} > {t('refresh')} setCleanupOpen(true)} disabled={loading || cleaning || total === 0} > {t('clear')} {t('backToNodes')} {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')} { setPageSize(Number.parseInt(value, 10)); setPageNo(1); }} > {PAGE_SIZE_OPTIONS.map((option) => ( {t('pageSizeOption', { count: option })} ))} {t('filter')} {t('reset')} {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 || '—'} { setSelectedLog(log); setDetailOpen(true); }} > {t('detail')} ))} )} {!loading && rows.length > 0 ? ( {t('pageInfo', { current, total: Math.max(totalPage, 1), count: total, })} setPageNo((prev) => Math.max(1, prev - 1))} > {t('prev')} = totalPage} onClick={() => setPageNo((prev) => totalPage > 0 ? Math.min(totalPage, prev + 1) : prev, ) } > {t('next')} ) : null} {t('clearTitle')} {t('clearDesc', { count: total })} {tc('cancel')} { event.preventDefault(); void handleCleanupLogs(); }} > {cleaning ? t('clearing') : t('confirmClear')} ); }
{t('description')}
{item.label}
{item.value}
{t('nodeId')}
{t('pageSize')}
{t('pageInfo', { current, total: Math.max(totalPage, 1), count: total, })}