"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 ? "清空中..." : "确认清空"}
) }