'use client'; import Link from 'next/link'; import {useRouter} from 'next/navigation'; import {useMutation, useQueryClient} from '@tanstack/react-query'; import {useState} from 'react'; import {ArrowLeft, FileText, RefreshCw, RotateCcw, Trash2, Upload,} from 'lucide-react'; import {toast} from 'sonner'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import {Button} from '@/components/ui/button'; import {Card, CardContent, CardDescription, CardHeader, CardTitle,} from '@/components/ui/card'; import {formatDateTime} from '@/lib/utils'; import type {NodeAgentReleaseInfo, NodeItem, ReleaseChannel} from '@/lib/services/openflare'; import {NodeService} from '@/lib/services/openflare'; import {AgentUpdateDialog} from './agent-update-dialog'; import {NodeEditorDialog} from './node-editor-dialog'; import {NodeObservability} from './node-observability'; import {NodeStatusBadge} from './node-status-badge'; import { formatRelativeTime, getApplyLabel, getApplyTone, getErrorMessage, getNodeStatusLabel, getNodeStatusTone, getOpenrestyStatusLabel, getOpenrestyStatusTone, isMeaningfulTime, } from './node-utils'; const nodesQueryKey = ['openflare', 'nodes']; export function EdgeNodeDetail({ node }: { node: NodeItem }) { const router = useRouter(); const queryClient = useQueryClient(); const [editorOpen, setEditorOpen] = useState(false); const [upgradeOpen, setUpgradeOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const saveMutation = useMutation({ mutationFn: (payload: Parameters[1]) => NodeService.updateNode(node.id, payload), onSuccess: async () => { toast.success('节点已更新'); setEditorOpen(false); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error)), }); const forceSyncMutation = useMutation({ mutationFn: () => NodeService.requestForceSync(node.id), onSuccess: async (updated) => { toast.success(`已向节点 ${updated.name} 下发强制同步指令`); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error)), }); const restartMutation = useMutation({ mutationFn: () => NodeService.requestOpenrestyRestart(node.id), onSuccess: async (updated) => { toast.success(`已向节点 ${updated.name} 下发 OpenResty 重启指令`); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error)), }); const upgradeMutation = useMutation({ mutationFn: ({ release, channel, }: { release: NodeAgentReleaseInfo | null; channel: ReleaseChannel; }) => NodeService.requestAgentUpdate(node.id, { channel: release?.channel ?? channel, tag_name: release?.channel === 'preview' ? release.tag_name || undefined : undefined, }), onSuccess: async (updated) => { toast.success( `已向节点 ${updated.name} 下发${updated.update_channel === 'preview' ? '预览版' : '正式版'}升级指令`, ); setUpgradeOpen(false); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error)), }); const deleteMutation = useMutation({ mutationFn: () => NodeService.deleteNode(node.id), onSuccess: async () => { toast.success('节点已删除'); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); router.push('/nodes'); }, onError: (error) => toast.error(getErrorMessage(error)), }); const handleRefresh = () => { void Promise.all([ queryClient.invalidateQueries({ queryKey: nodesQueryKey }), queryClient.invalidateQueries({ queryKey: ['openflare', 'node-observability', node.id] }), ]); }; return (

{node.name}

节点 ID {node.node_id} Agent 版本 {node.version || 'unknown'} 当前配置版本 {node.current_version || '未应用'} 最近心跳 {isMeaningfulTime(node.last_seen_at) ? formatRelativeTime(node.last_seen_at) : '暂无'}
节点信息 边缘代理节点 (edge_node)
运行状态
OpenResty 健康
最近应用
IP 地址 {node.ip || '—'} {node.ip_manual_override ? '(已锁定)' : ''}
地图点位 {node.geo_name || '未配置'}
OpenResty 状态消息 {node.openresty_message || '无'}
创建时间 {formatDateTime(node.created_at)}
更新时间 {formatDateTime(node.updated_at)}
{node.last_error ? (
{node.last_error}
) : null}
setEditorOpen(false)} onSubmit={async (payload) => { await saveMutation.mutateAsync(payload); }} /> setUpgradeOpen(false)} onConfirm={async (release, channel) => { await upgradeMutation.mutateAsync({ release, channel }); }} /> 确认删除节点 确认删除节点「{node.name}」吗?删除后该节点需要重新创建并重新接入。 取消 deleteMutation.mutate()} > {deleteMutation.isPending ? '删除中...' : '确认删除'}
); }