'use client'; import Link from 'next/link'; import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { AppModal } from '@/components/ui/app-modal'; import { EmptyState } from '@/components/feedback/empty-state'; import { ErrorState } from '@/components/feedback/error-state'; import { InlineMessage } from '@/components/feedback/inline-message'; import { LoadingState } from '@/components/feedback/loading-state'; import { PageHeader } from '@/components/layout/page-header'; import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; import { cleanupApplyLogs, getApplyLogs, } from '@/features/apply-logs/api/apply-logs'; import type { ApplyLogCleanupPayload, ApplyLogItem, } from '@/features/apply-logs/types'; import { PrimaryButton, ResourceField, ResourceInput, ResourceSelect, SecondaryButton, } from '@/features/shared/components/resource-primitives'; import { formatDateTime, formatRelativeTime } from '@/lib/utils/date'; const applyLogsQueryKey = (nodeId: string, pageNo: number, pageSize: number) => ['apply-logs', nodeId, pageNo, pageSize] as const; const pageSizeOptions = [20, 50, 100]; const emptyApplyLogRows: ApplyLogItem[] = []; type FeedbackState = { tone: 'info' | 'success' | 'danger'; message: string; }; function getErrorMessage(error: unknown) { return error instanceof Error ? error.message : '请求失败,请稍后重试。'; } function getResultMeta(result: string) { if (result === 'success') { return { label: '成功', variant: 'success' as const }; } if (result === 'warning') { return { label: '警告', variant: 'warning' as const }; } return { label: '失败', variant: 'danger' as const }; } function truncateHash(value: string) { if (!value) { return '—'; } return value.length > 12 ? `${value.slice(0, 12)}...` : value; } function buildSummary( rows: ApplyLogItem[], total: number, current: number, totalPage: number, ) { const nodeIds = new Set(rows.map((item) => item.node_id)); return [ { label: '总记录数', value: total }, { label: '当前页', value: current }, { label: '总页数', value: totalPage }, { label: '当前页节点数', value: nodeIds.size }, ]; } export function ApplyLogsPage() { const queryClient = useQueryClient(); const [nodeFilterInput, setNodeFilterInput] = useState(''); const [nodeFilter, setNodeFilter] = useState(''); const [pageNo, setPageNo] = useState(1); const [pageSize, setPageSize] = useState(20); const [selectedLog, setSelectedLog] = useState(null); const [isCleanupModalOpen, setCleanupModalOpen] = useState(false); const [cleanupMode, setCleanupMode] = useState<'all' | 'custom'>('custom'); const [customRetentionDays, setCustomRetentionDays] = useState('30'); const [feedback, setFeedback] = useState(null); useEffect(() => { const nodeID = new URLSearchParams(window.location.search) .get('node_id') ?.trim(); if (!nodeID) { return; } setNodeFilterInput(nodeID); setNodeFilter(nodeID); setPageNo(1); }, []); const logsQuery = useQuery({ queryKey: applyLogsQueryKey(nodeFilter, pageNo, pageSize), queryFn: () => getApplyLogs({ node_id: nodeFilter || undefined, pageNo, pageSize, }), placeholderData: (previous) => previous, }); const cleanupMutation = useMutation({ mutationFn: (payload: ApplyLogCleanupPayload) => cleanupApplyLogs(payload), onSuccess: async (result) => { setCleanupModalOpen(false); setPageNo(1); setFeedback({ tone: 'success', message: result.delete_all ? `已删除全部应用日志,共 ${result.deleted_count} 条。` : `已清理保留期之外的应用日志,共 ${result.deleted_count} 条。`, }); await queryClient.invalidateQueries({ queryKey: ['apply-logs'] }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const rows = logsQuery.data?.rows ?? emptyApplyLogRows; const current = logsQuery.data?.current ?? pageNo; const total = logsQuery.data?.total ?? 0; const totalPage = logsQuery.data?.totalPage ?? 0; const summary = useMemo( () => buildSummary(rows, total, current, totalPage), [rows, total, current, totalPage], ); useEffect(() => { if (rows.length === 0 && selectedLog) { setSelectedLog(null); return; } if (selectedLog && !rows.some((item) => item.id === selectedLog.id)) { setSelectedLog(null); } }, [rows, selectedLog]); const handleSearch = () => { setFeedback(null); setPageNo(1); setNodeFilter(nodeFilterInput.trim()); }; const handleReset = () => { setFeedback(null); setNodeFilter(''); setNodeFilterInput(''); setPageNo(1); }; const handleRefresh = async () => { setFeedback(null); await queryClient.invalidateQueries({ queryKey: applyLogsQueryKey(nodeFilter, pageNo, pageSize), }); }; const handleCleanupConfirm = () => { const payload: ApplyLogCleanupPayload = cleanupMode === 'all' ? { delete_all: true } : { retention_days: Number.parseInt(customRetentionDays, 10), }; cleanupMutation.mutate(payload); }; return (
刷新 { setFeedback(null); setCleanupModalOpen(true); }} > 删除日志 返回节点
} /> {feedback ? ( ) : null}
{summary.map((item) => (

{item.label}

{item.value}

))}
setNodeFilterInput(event.target.value)} placeholder="输入 node_id 过滤应用日志" /> { setPageSize(Number.parseInt(event.target.value, 10)); setPageNo(1); }} > {pageSizeOptions.map((option) => ( ))}
筛选 清空
{logsQuery.isLoading ? ( ) : logsQuery.isError ? ( ) : rows.length === 0 ? ( ) : ( <>
{rows.map((log) => { const resultMeta = getResultMeta(log.result); return ( ); })}
Node ID 版本 结果 Checksum 时间 消息 操作
{log.node_id} {log.version} {truncateHash(log.checksum)}

{formatDateTime(log.created_at)}

{formatRelativeTime(log.created_at)}

{log.message || '—'}
setSelectedLog(log)} > 查看详情

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

setPageNo((previous) => Math.max(1, previous - 1)) } > 上一页 = totalPage} onClick={() => setPageNo((previous) => totalPage > 0 ? Math.min(totalPage, previous + 1) : previous, ) } > 下一页
)}
setSelectedLog(null)} > {selectedLog ? (

消息

                {selectedLog.message || '—'}
              
) : null}
setCleanupModalOpen(false)} footer={
setCleanupModalOpen(false)} > 取消 {cleanupMutation.isPending ? '处理中...' : '确认删除'}
} >
{cleanupMode === 'custom' ? ( setCustomRetentionDays(event.target.value)} type="number" min={1} max={3650} placeholder="输入保留天数" /> ) : null} {cleanupMutation.isError ? ( ) : null}
); } function MetricCard({ label, value, breakAll = false, }: { label: string; value: string; breakAll?: boolean; }) { return (

{label}

{value}

); }