'use client'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { Activity, Cpu, FileText, Fingerprint, Package, 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 { 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 { NodeDetailShell } from './node-detail-shell'; import { NodeErrorBanner, NodeInfoRow, NodeSectionCard, } from './node-detail-primitives'; 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], }), ]); }; const headerActions = ( <> ); const overviewTab = (
{node.last_error ? : null}
{isMeaningfulTime(node.last_seen_at) ? `${formatRelativeTime(node.last_seen_at)} · ${formatDateTime(node.last_seen_at)}` : '暂无'} {node.ip || '—'} {node.ip_manual_override ? '(已锁定)' : ''} {node.geo_name || '未配置'}
应用记录 } >
{node.current_version || '未应用'} {isMeaningfulTime(node.latest_apply_at) ? `${formatRelativeTime(node.latest_apply_at)} · ${formatDateTime(node.latest_apply_at)}` : '暂无'} {node.version || 'unknown'} {node.ext_version || 'unknown'} {node.auto_update_enabled ? '已启用' : '手动'}
); const manageTab = (
{node.node_id} {node.access_token || '暂无'} {formatDateTime(node.created_at)} {formatDateTime(node.updated_at)}

OpenResty 状态消息

{node.openresty_message || '当前未上报额外错误。'}

{node.restart_openresty_requested ? (
已等待节点在下一次心跳后执行 OpenResty 重启。
) : null}
); return ( <> } manage={manageTab} defaultTab='overview' /> 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 ? '删除中...' : '确认删除'} ); }