'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 { 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 getResultBadge(result: string) { if (result === 'success') { return ( 成功 ); } if (result === 'warning') { return ( 警告 ); } return ( 失败 ); } export function ApplyLogsPageClient() { 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: '总记录数', value: total }, { label: '当前页', value: current }, { label: '总页数', value: totalPage }, { label: '当前页节点数', value: nodeIds.size }, ]; }, [rows, total, current, totalPage]); 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 : '加载失败'); } finally { setLoading(false); } }, [nodeFilter, pageNo, pageSize]); 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('应用日志已清空', { description: `共删除 ${result.deleted_count} 条记录`, }); setCleanupOpen(false); setPageNo(1); await fetchLogs(); } catch (err) { toast.error('清空应用日志失败', { description: err instanceof Error ? err.message : '未知错误', }); } finally { setCleaning(false); } }; return (

应用日志

查看节点应用配置的成功、警告和失败记录,支持按 node_id 过滤。

{summary.map((item) => (

{item.label}

{item.value}

))}

Node ID

setNodeFilterInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSearch(); }} placeholder='输入 node_id 过滤应用日志' className='pl-8 h-9 text-xs' />

每页条数

{error ? ( void fetchLogs()} /> ) : null}
{loading ? ( ) : rows.length === 0 ? ( ) : ( Node ID 版本 结果 Checksum 时间 消息 操作 {rows.map((log) => ( {log.node_id} {log.version} {getResultBadge(log.result)} {truncateHash(log.checksum)} {formatDateTime(log.created_at)}
{log.message || '—'}
))}
)}
{!loading && rows.length > 0 ? (

第 {current} / {Math.max(totalPage, 1)} 页,共 {total} 条记录。

) : null} 确认清空应用日志? 该操作将删除全部 {total}{' '} 条应用日志记录,且不可撤销。清空后节点仍会照常上报新的应用结果。 取消 { event.preventDefault(); void handleCleanupLogs(); }} > {cleaning ? '清空中...' : '确认清空'}
); }