'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 { useTranslations } from 'next-intl'; 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 { InstallCommand } from './install-command'; 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 t = useTranslations('nodes'); const tc = useTranslations('common'); 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(t('edge.toastUpdated')); setEditorOpen(false); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))), }); const forceSyncMutation = useMutation({ mutationFn: () => NodeService.requestForceSync(node.id), onSuccess: async (updated) => { toast.success(t('edge.toastForceSync', { name: updated.name })); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))), }); const restartMutation = useMutation({ mutationFn: () => NodeService.requestOpenrestyRestart(node.id), onSuccess: async (updated) => { toast.success(t('edge.toastRestart', { name: updated.name })); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))), }); 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( t('edge.toastUpgrade', { name: updated.name, channel: updated.update_channel === 'preview' ? t('channel.preview') : t('channel.stable'), }), ); setUpgradeOpen(false); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))), }); const deleteMutation = useMutation({ mutationFn: () => NodeService.deleteNode(node.id), onSuccess: async () => { toast.success(t('edge.toastDeleted')); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); router.push('/nodes'); }, onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))), }); const handleRefresh = () => { void Promise.all([ queryClient.invalidateQueries({ queryKey: nodesQueryKey }), queryClient.invalidateQueries({ queryKey: ['openflare', 'node-observability', node.id], }), ]); }; const headerActions = ( <> ); const manageTab = (
{node.last_error ? : null}
{isMeaningfulTime(node.last_seen_at) ? `${formatRelativeTime(node.last_seen_at, t)} · ${formatDateTime(node.last_seen_at)}` : t('na')} {node.ip || '—'} {node.ip_manual_override ? t('lockedSuffix') : ''} {node.geo_name || t('notConfigured')}
{t('applyLogs')} } >
{node.current_version || t('notApplied')} {isMeaningfulTime(node.latest_apply_at) ? `${formatRelativeTime(node.latest_apply_at, t)} · ${formatDateTime(node.latest_apply_at)}` : t('na')} {node.version || 'unknown'} {node.ext_version || 'unknown'} {node.auto_update_enabled ? t('enabled') : t('manual')}

{t('edge.openrestyMessage')}

{node.openresty_message || t('edge.noExtraError')}

{node.restart_openresty_requested ? (
{t('edge.restartPending')}
) : 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 }); }} /> {t('deleteTitle')} {t('deleteDesc', { name: node.name })} {tc('cancel')} deleteMutation.mutate()} > {deleteMutation.isPending ? t('deleting') : t('confirmDelete')} ); }