'use client'; import { useEffect } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslations } from 'next-intl'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import type { NodeItem, NodeMutationPayload, NodeType, } from '@/lib/services/openflare'; function buildNodeSchema(t: (key: string) => string) { return z .object({ node_type: z.enum(['edge_node', 'tunnel_relay', 'tunnel_client']), name: z.string().trim().min(1, t('editor.errName')).max(255), ip: z.string(), ip_manual_override: z.boolean(), auto_update_enabled: z.boolean(), geo_manual_override: z.boolean(), geo_name: z.string(), geo_latitude: z.string(), geo_longitude: z.string(), relay_bind_port: z.string(), relay_vhost_http_port: z.string(), }) .superRefine((value, context) => { if (value.ip_manual_override && !value.ip.trim()) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['ip'], message: t('editor.errLockIp'), }); } if (value.node_type === 'tunnel_relay') { const bindPort = Number(value.relay_bind_port); const vhostPort = Number(value.relay_vhost_http_port); if (!Number.isFinite(bindPort) || bindPort <= 0) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['relay_bind_port'], message: t('editor.errBindPort'), }); } if (!Number.isFinite(vhostPort) || vhostPort <= 0) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['relay_vhost_http_port'], message: t('editor.errVhostPort'), }); } } }); } type NodeFormValues = z.infer>; const defaultForm: NodeFormValues = { node_type: 'edge_node', name: '', ip: '', ip_manual_override: false, auto_update_enabled: false, geo_manual_override: false, geo_name: '', geo_latitude: '', geo_longitude: '', relay_bind_port: '7000', relay_vhost_http_port: '8080', }; function buildFormValues(node?: NodeItem | null): NodeFormValues { if (!node) return defaultForm; return { node_type: node.node_type, name: node.name, ip: node.ip, ip_manual_override: node.ip_manual_override, auto_update_enabled: node.auto_update_enabled, geo_manual_override: node.geo_manual_override, geo_name: node.geo_name, geo_latitude: node.geo_latitude === undefined || node.geo_latitude === null ? '' : String(node.geo_latitude), geo_longitude: node.geo_longitude === undefined || node.geo_longitude === null ? '' : String(node.geo_longitude), relay_bind_port: String(node.relay_bind_port || 7000), relay_vhost_http_port: String(node.relay_vhost_http_port || 8080), }; } function toPayload(form: NodeFormValues): NodeMutationPayload { const base: NodeMutationPayload = { node_type: form.node_type, name: form.name.trim(), ip: form.ip.trim(), ip_manual_override: form.ip_manual_override, auto_update_enabled: form.auto_update_enabled, geo_manual_override: form.geo_manual_override, geo_name: form.geo_manual_override ? form.geo_name.trim() : '', geo_latitude: form.geo_manual_override && form.geo_latitude ? Number(form.geo_latitude) : null, geo_longitude: form.geo_manual_override && form.geo_longitude ? Number(form.geo_longitude) : null, }; if (form.node_type === 'tunnel_relay') { return { ...base, relay_bind_port: Number(form.relay_bind_port), relay_vhost_http_port: Number(form.relay_vhost_http_port), relay_web_server_enabled: true, }; } return base; } export function NodeEditorDialog({ open, node, submitting, onClose, onSubmit, }: { open: boolean; node: NodeItem | null; submitting: boolean; onClose: () => void; onSubmit: (payload: NodeMutationPayload) => Promise; }) { const t = useTranslations('nodes'); const tc = useTranslations('common'); const form = useForm({ resolver: zodResolver(buildNodeSchema(t)), defaultValues: defaultForm, }); useEffect(() => { if (open) { form.reset(buildFormValues(node)); } }, [form, open, node]); const nodeType = form.watch('node_type'); const ipManualOverride = form.watch('ip_manual_override'); const geoManualOverride = form.watch('geo_manual_override'); const handleSubmit = form.handleSubmit(async (values) => { await onSubmit(toPayload(values)); }); return ( !next && onClose()}> {node ? t('editor.editTitle') : t('editor.createTitle')} {t('editor.description')}
void handleSubmit(event)} className='space-y-4' >
{form.formState.errors.name ? (

{form.formState.errors.name.message}

) : null}
{form.formState.errors.ip ? (

{form.formState.errors.ip.message}

) : null}

{t('editor.lockIp')}

{t('editor.lockIpDesc')}

form.setValue('ip_manual_override', checked) } />

{t('editor.autoUpdate')}

{t('editor.autoUpdateDesc')}

form.setValue('auto_update_enabled', checked) } />
{nodeType === 'tunnel_relay' ? (
{form.formState.errors.relay_bind_port ? (

{form.formState.errors.relay_bind_port.message}

) : null}
{form.formState.errors.relay_vhost_http_port ? (

{form.formState.errors.relay_vhost_http_port.message}

) : null}
) : null}

{t('editor.manualGeo')}

{t('editor.manualGeoDesc')}

form.setValue('geo_manual_override', checked) } />
{geoManualOverride ? (
) : null}
); }