import { updateServer } from "@/api/server" import { Button } from "@/components/ui/button" import { Calendar } from "@/components/ui/calendar" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { ScrollArea } from "@/components/ui/scroll-area" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" import { IconButton } from "@/components/xui/icon-button" import { type PublicNote, PublicNoteSchema, applyPublicNoteDate, applyPublicNotePatch, detectPublicNoteMode, normalizeISO, parsePublicNote, toggleEndNoExpiry, validatePublicNote, } from "@/lib/public-note" import { conv } from "@/lib/utils" import { asOptionalField } from "@/lib/utils" import { ModelServer } from "@/types" import { zodResolver } from "@hookform/resolvers/zod" import { HelpCircle } from "lucide-react" import { useState } from "react" import { useForm, useWatch } from "react-hook-form" import { useTranslation } from "react-i18next" import { toast } from "sonner" import { KeyedMutator } from "swr" import { z } from "zod" interface ServerCardProps { data: ModelServer mutate: KeyedMutator } const serverFormSchema = z.object({ name: z.string().min(1), note: asOptionalField(z.string()), public_note: asOptionalField( z.string().refine( (val) => { const s = (val ?? "").trim() if (s.length === 0) return true try { const obj = JSON.parse(s) if (typeof obj !== "object" || obj === null) return true return PublicNoteSchema.safeParse(obj).success } catch { // skip check if not JSON return true } }, { message: "Invalid Public Note JSON" }, ), ), display_index: z.coerce.number().int(), hide_for_guest: asOptionalField(z.boolean()), enable_ddns: asOptionalField(z.boolean()), ddns_profiles: asOptionalField(z.array(z.number())), ddns_profiles_raw: asOptionalField(z.string()), override_ddns_domains: asOptionalField(z.record(z.string(), z.array(z.string()))), override_ddns_domains_raw: asOptionalField( z.string().refine( (val) => { try { JSON.parse(val) return true } catch { return false } }, { message: "Invalid JSON string", }, ), ), }) export const ServerCard: React.FC = ({ data, mutate }) => { const { t } = useTranslation() const form = useForm({ resolver: zodResolver(serverFormSchema) as any, defaultValues: { ...data, ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined, override_ddns_domains_raw: data.override_ddns_domains ? JSON.stringify(data.override_ddns_domains) : undefined, }, resetOptions: { keepDefaultValues: false, }, }) const enableDDNS = useWatch({ control: form.control, name: "enable_ddns" }) const [open, setOpen] = useState(false) const [publicNoteObj, setPublicNoteObj] = useState( parsePublicNote(data?.public_note), ) const [publicNoteErrors, setPublicNoteErrors] = useState< Partial< Record< | "billing.startDate" | "billing.endDate" | "billing.autoRenewal" | "billing.cycle" | "billing.amount" | "plan.bandwidth" | "plan.trafficVol" | "plan.trafficType" | "plan.IPv4" | "plan.IPv6" | "plan.extra", string > > >({}) const [publicNoteMode, setPublicNoteMode] = useState<"structured" | "raw">( detectPublicNoteMode(data?.public_note), ) const [publicNoteRaw, setPublicNoteRaw] = useState(data?.public_note ?? "") const patchPublicNote = (path: string, value: string | undefined) => { setPublicNoteObj((prev) => applyPublicNotePatch(prev, path, value)) } const patchPublicNoteDate = ( path: "billingDataMod.startDate" | "billingDataMod.endDate", d: Date, ) => { setPublicNoteObj((prev) => applyPublicNoteDate(prev, path, d)) } const toggleEndNoExpiryLocal = () => { setPublicNoteObj((prev) => toggleEndNoExpiry(prev)) } const onSubmit = async (values: any) => { try { values.ddns_profiles = values.ddns_profiles_raw ? conv.strToArr(values.ddns_profiles_raw).map(Number) : undefined values.override_ddns_domains = values.override_ddns_domains_raw ? JSON.parse(values.override_ddns_domains_raw) : undefined if (publicNoteMode === "raw") { const raw = (publicNoteRaw ?? "").trim() if (raw.length === 0) { values.public_note = undefined } else { values.public_note = raw } } else { const { errors, valid } = validatePublicNote(publicNoteObj) if (!valid) { setPublicNoteErrors(errors) toast(t("Error"), { description: t("Validation.InvalidForm") }) return } setPublicNoteErrors({}) const bd = publicNoteObj.billingDataMod const pd = publicNoteObj.planDataMod const pnNormalized: PublicNote = { billingDataMod: bd && { ...bd, startDate: normalizeISO(bd.startDate), endDate: normalizeISO(bd.endDate), }, planDataMod: pd, } const jsonStr = JSON.stringify(pnNormalized) values.public_note = jsonStr.length > 2 ? jsonStr : undefined } await updateServer(data!.id!, values) } catch (e) { console.error(e) toast(t("Error"), { description: t("Results.UnExpectedError"), }) return } setOpen(false) await mutate() form.reset() } const handleOpenChange = (v: boolean) => { if (v) { form.reset({ ...data, ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined, override_ddns_domains_raw: data.override_ddns_domains ? JSON.stringify(data.override_ddns_domains) : undefined, }) setPublicNoteObj(parsePublicNote(data?.public_note)) setPublicNoteRaw(data?.public_note ?? "") setPublicNoteMode(detectPublicNoteMode(data?.public_note)) setPublicNoteErrors({}) } setOpen(v) } return ( e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()} >
{t("EditServer")}
( {t("Name")} )} /> ( {t("Weight")} )} /> {enableDDNS ? ( <> ( {t("DDNSProfiles") + t("SeparateWithComma")} )} /> ( {t("OverrideDDNSDomains")}