import { useState, useEffect } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { MainLayout } from "@/components/layouts/main-layout"; import { DnsRecord, InsertDnsRecord, Domain, recordTypes } from "@shared/schema"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription, } from "@/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; import { RecentActivity } from "@/components/activity/recent-activity"; import { Pagination } from "@/components/shared/pagination"; import { Loader2, Home, Plus, Pencil, Trash2, ArrowLeft, FileText } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; import { Link } from "wouter"; // DNS Record form schema const dnsRecordSchema = z.object({ name: z.string().min(1, "Record name is required"), type: z.enum(recordTypes), content: z.string().min(1, "Content is required"), ttl: z.number().int().min(1).default(3600), proxied: z.boolean().default(false), isActive: z.boolean().default(true), isAutoIP: z.boolean().default(false), notes: z.string().optional(), }); export default function DnsRecordsPage() { const { toast } = useToast(); const [domainId, setDomainId] = useState(null); // Get domainId from query params useEffect(() => { const params = new URLSearchParams(window.location.search); const id = params.get("domainId"); if (id) { setDomainId(id); } }, []); const [isAddRecordDialogOpen, setIsAddRecordDialogOpen] = useState(false); const [isEditRecordDialogOpen, setIsEditRecordDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedRecord, setSelectedRecord] = useState(null); const [currentPage, setCurrentPage] = useState(1); const pageSize = 10; // Fetch domain details const { data: domain } = useQuery({ queryKey: ["/api/domains", domainId], enabled: !!domainId, }); // Fetch DNS records for domain const { data: records = [], isLoading } = useQuery({ queryKey: ["/api/dns-records", domainId], enabled: !!domainId, }); // Form for adding/editing a DNS record const form = useForm>({ resolver: zodResolver(dnsRecordSchema), defaultValues: { name: "", type: "A", content: "", ttl: 3600, proxied: false, isActive: true, isAutoIP: false, notes: "", }, }); // Reset form when selected record changes useEffect(() => { if (selectedRecord) { form.reset({ name: selectedRecord.name, type: selectedRecord.type as any, content: selectedRecord.content, ttl: selectedRecord.ttl ?? 3600, proxied: selectedRecord.proxied ?? false, isActive: selectedRecord.isActive, isAutoIP: selectedRecord.isAutoIP ?? false, notes: selectedRecord.notes ?? "", }); } else { form.reset({ name: "", type: "A", content: "", ttl: 3600, proxied: false, isActive: true, isAutoIP: false, notes: "", }); } }, [selectedRecord, form]); // Add DNS record mutation const addRecordMutation = useMutation({ mutationFn: async (data: z.infer) => { if (!domainId) throw new Error("Domain ID is required"); const recordData: InsertDnsRecord = { ...data, domainId, }; const res = await apiRequest("POST", "/api/dns-records", recordData); return await res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/dns-records", domainId] }); setIsAddRecordDialogOpen(false); form.reset(); toast({ title: "Record added", description: "The DNS record has been successfully added.", }); }, onError: (error) => { toast({ title: "Failed to add record", description: error.message, variant: "destructive", }); }, }); // Update DNS record mutation const updateRecordMutation = useMutation({ mutationFn: async ({ id, data }: { id: string, data: z.infer }) => { const res = await apiRequest("PUT", `/api/dns-records/${id}`, data); return await res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/dns-records", domainId] }); setIsEditRecordDialogOpen(false); setSelectedRecord(null); toast({ title: "Record updated", description: "The DNS record has been successfully updated.", }); }, onError: (error) => { toast({ title: "Failed to update record", description: error.message, variant: "destructive", }); }, }); // Delete DNS record mutation const deleteRecordMutation = useMutation({ mutationFn: async (recordId: string) => { await apiRequest("DELETE", `/api/dns-records/${recordId}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/dns-records", domainId] }); setIsDeleteDialogOpen(false); setSelectedRecord(null); toast({ title: "Record deleted", description: "The DNS record has been successfully deleted.", }); }, onError: (error) => { toast({ title: "Failed to delete record", description: error.message, variant: "destructive", }); }, }); // Form submission handler for adding const onAddSubmit = (data: z.infer) => { addRecordMutation.mutate(data); }; // Form submission handler for editing const onEditSubmit = (data: z.infer) => { if (selectedRecord) { updateRecordMutation.mutate({ id: selectedRecord.id, data }); } }; // Handle record editing const handleEditRecord = (record: DnsRecord) => { setSelectedRecord(record); setIsEditRecordDialogOpen(true); }; // Handle record deletion const handleDeleteRecord = (record: DnsRecord) => { setSelectedRecord(record); setIsDeleteDialogOpen(true); }; // Confirm deletion const confirmDeleteRecord = () => { if (selectedRecord) { deleteRecordMutation.mutate(selectedRecord.id); } }; // Calculate pagination const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedRecords = records.slice(startIndex, endIndex); const totalPages = Math.ceil(records.length / pageSize); if (!domainId) { return (

No Domain Selected

Please select a domain to manage its DNS records.

); } return ( {/* Breadcrumb */}
Domains {domain?.name || 'Loading...'}
DNS Records {isLoading ? (
) : records.length === 0 ? (

No DNS records found

Get started by adding your first DNS record.

) : ( <>
Name Type Content TTL Status Updated Actions {paginatedRecords.map((record) => ( {record.name} {record.type} {record.isAutoIP && (record.type === 'A' || record.type === 'AAAA') && ( Auto IP )} {record.content.length > 30 ? record.content.substring(0, 30) + '...' : record.content} {record.ttl} {record.isActive ? ( Active ) : ( Inactive )} {record.lastUpdated ? formatDistanceToNow(new Date(record.lastUpdated), { addSuffix: true }) : "Never"}
{record.notes && (

{record.notes}

)}
))}
{records.length > pageSize && (
)} )}
{/* Add Record Dialog */} Add DNS Record Add a new DNS record to {domain?.name || 'your domain'}.
( Name Use @ for root domain or enter subdomain name )} /> ( Record Type )} /> ( Content {form.watch("type") === "A" && "IP address (e.g. 192.168.1.1)"} {form.watch("type") === "AAAA" && "IPv6 address"} {form.watch("type") === "CNAME" && "Domain name (e.g. example.com)"} {form.watch("type") === "MX" && "Mail server (e.g. mail.example.com)"} {form.watch("type") === "TXT" && "Text content"} )} /> ( TTL (seconds) field.onChange(parseInt(e.target.value || "3600"))} /> Time-to-live in seconds. 3600 = 1 hour )} /> (
Proxied Enable proxying through CDN (Cloudflare only)
)} /> (
Active Enable this record to be used
)} /> {(form.watch("type") === "A" || form.watch("type") === "AAAA") && ( (
Auto IP Address Automatically determine IP address using STUN
)} /> )} ( Notes Add any additional information about this record )} />
{/* Edit Record Dialog */} Edit DNS Record Update the DNS record for {domain?.name || 'your domain'}.
( Name Use @ for root domain or enter subdomain name )} /> ( Record Type )} /> ( Content {form.watch("type") === "A" && "IP address (e.g. 192.168.1.1)"} {form.watch("type") === "AAAA" && "IPv6 address"} {form.watch("type") === "CNAME" && "Domain name (e.g. example.com)"} {form.watch("type") === "MX" && "Mail server (e.g. mail.example.com)"} {form.watch("type") === "TXT" && "Text content"} )} /> ( TTL (seconds) field.onChange(parseInt(e.target.value || "3600"))} /> Time-to-live in seconds. 3600 = 1 hour )} /> (
Proxied Enable proxying through CDN (Cloudflare only)
)} /> (
Active Enable this record to be used
)} /> {(form.watch("type") === "A" || form.watch("type") === "AAAA") && ( (
Auto IP Address Automatically determine IP address using STUN
)} /> )} ( Notes Add any additional information about this record )} />
{/* Delete Confirmation Dialog */} Are you sure? This will permanently delete the DNS record {selectedRecord?.name} of type {selectedRecord?.type}. This action cannot be undone. Cancel {deleteRecordMutation.isPending ? ( <> Deleting... ) : ( "Delete" )}
); }