'use client'; import Link from 'next/link'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { Loader2, Plus, RefreshCw, Server } 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 { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { EmptyStateWithBorder } from '@/components/layout/empty'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import type { NodeItem, NodeMutationPayload } from '@/lib/services/openflare'; import { NodeService } from '@/lib/services/openflare'; import { NodeEditorDialog } from './components/node-editor-dialog'; import { filterNodesByType, getFilterDescription, getNodeFilter, NodeTypeFilter, } from './components/node-type-filter'; import { NodesTable } from './components/nodes-table'; import { getErrorMessage } from './components/node-utils'; const nodesQueryKey = ['openflare', 'nodes']; export function NodesPageClient() { const t = useTranslations('nodes'); const tc = useTranslations('common'); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const [editingNode, setEditingNode] = useState(null); const [editorOpen, setEditorOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const nodeFilter = useMemo( () => getNodeFilter(new URLSearchParams(searchParams.toString())), [searchParams], ); const nodesQuery = useQuery({ queryKey: nodesQueryKey, queryFn: () => NodeService.listNodes(), refetchInterval: 5000, }); const nodes = useMemo(() => nodesQuery.data ?? [], [nodesQuery.data]); const filteredNodes = useMemo( () => filterNodesByType(nodes, nodeFilter), [nodeFilter, nodes], ); const saveMutation = useMutation({ mutationFn: async (payload: NodeMutationPayload) => { if (editingNode) { return NodeService.updateNode(editingNode.id, payload); } return NodeService.createNode(payload); }, onSuccess: async () => { toast.success(editingNode ? t('updated') : t('created')); setEditingNode(null); setEditorOpen(false); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => { toast.error(getErrorMessage(error, t('requestFailed'))); }, }); const deleteMutation = useMutation({ mutationFn: (id: number) => NodeService.deleteNode(id), onSuccess: async () => { toast.success(t('deleted')); setDeleteTarget(null); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => { toast.error(getErrorMessage(error, t('requestFailed'))); }, }); const handleCreate = () => { setEditingNode(null); setEditorOpen(true); }; const handleEdit = (node: NodeItem) => { setEditingNode(node); setEditorOpen(true); }; const handleRefresh = () => { void queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }; return (

{t('title')}

{t('listTitle')} {getFilterDescription(nodeFilter, t)}
{nodesQuery.isLoading ? ( ) : nodesQuery.isError ? (
) : filteredNodes.length === 0 ? ( ) : ( )}
{ setEditorOpen(false); setEditingNode(null); }} onSubmit={async (payload) => { await saveMutation.mutateAsync(payload); }} /> !open && setDeleteTarget(null)} > {t('deleteTitle')} {t('deleteDesc', { name: deleteTarget?.name ?? '' })} {tc('cancel')} deleteTarget && deleteMutation.mutate(deleteTarget.id) } > {deleteMutation.isPending ? t('deleting') : t('confirmDelete')}
); }