diff --git a/messages/en-US.json b/messages/en-US.json index b2dbb302..24e86a36 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1900,5 +1900,8 @@ "unverified": "Unverified", "domainSetting": "Domain Settings", "domainSettingDescription": "Configure settings for your domain", - "preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver)." + "preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver).", + "recordName": "Record Name", + "auto": "Auto", + "TTL": "TTL" } diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index 1f1002af..d40a0cb8 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -290,7 +290,8 @@ export async function createOrgDomain( domainId, recordType: "NS", baseDomain: baseDomain, - value: nsValue + value: nsValue, + verified: false }); } } else if (type === "cname") { @@ -312,7 +313,8 @@ export async function createOrgDomain( domainId, recordType: "CNAME", baseDomain: cnameRecord.baseDomain, - value: cnameRecord.value + value: cnameRecord.value, + verified: false }); } } else if (type === "wildcard") { @@ -334,7 +336,8 @@ export async function createOrgDomain( domainId, recordType: "A", baseDomain: aRecord.baseDomain, - value: aRecord.value + value: aRecord.value, + verified: true }); } } diff --git a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx index 1e1fec7b..319ccdd5 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx @@ -42,7 +42,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
- +
diff --git a/src/components/DNSRecordTable.tsx b/src/components/DNSRecordTable.tsx new file mode 100644 index 00000000..dd9bcad4 --- /dev/null +++ b/src/components/DNSRecordTable.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { Button } from "@app/components/ui/button"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { useToast } from "@app/hooks/useToast"; +import { Badge } from "@app/components/ui/badge"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import { DNSRecordsDataTable } from "./DNSRecordsDataTable"; + +export type DNSRecordRow = { + id: string; + domainId: string; + recordType: string; // "NS" | "CNAME" | "A" | "TXT" + baseDomain: string | null; + value: string; + verified?: boolean; +}; + +type Props = { + records: DNSRecordRow[]; + domainId: string; + isRefreshing?: boolean; +}; + +export default function DNSRecordsTable({ records, domainId, isRefreshing }: Props) { + const t = useTranslations(); + + const columns: ColumnDef[] = [ + { + accessorKey: "baseDomain", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const baseDomain = row.original.baseDomain; + return ( +
+ {baseDomain || "-"} +
+ ); + } + }, + { + accessorKey: "recordType", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const type = row.original.recordType; + return ( +
+ {type} +
+ ); + } + }, + { + accessorKey: "ttl", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return ( +
+ {t("auto")} +
+ ); + } + }, + { + accessorKey: "value", + header: () => { + return
{t("value")}
; + }, + cell: ({ row }) => { + const value = row.original.value; + return ( +
+
+ {value} +
+
+ ); + } + }, + { + accessorKey: "verified", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const verified = row.original.verified; + return ( + verified ? ( + {t("verified")} + ) : ( + {t("unverified")} + ) + ); + } + } + ]; + + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/DNSRecordsDataTable.tsx b/src/components/DNSRecordsDataTable.tsx new file mode 100644 index 00000000..4e70a1b2 --- /dev/null +++ b/src/components/DNSRecordsDataTable.tsx @@ -0,0 +1,209 @@ +"use client"; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + getSortedRowModel, + getFilteredRowModel +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Button } from "@app/components/ui/button"; +import { useMemo, useState } from "react"; +import { Plus, RefreshCw } from "lucide-react"; +import { + Card, + CardContent, + CardHeader, + CardTitle +} from "@app/components/ui/card"; +import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs"; +import { useTranslations } from "next-intl"; +import { Badge } from "./ui/badge"; + + +type TabFilter = { + id: string; + label: string; + filterFn: (row: any) => boolean; +}; + +type DNSRecordsDataTableProps = { + columns: ColumnDef[]; + data: TData[]; + title?: string; + addButtonText?: string; + onAdd?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; + searchPlaceholder?: string; + searchColumn?: string; + defaultSort?: { + id: string; + desc: boolean; + }; + tabs?: TabFilter[]; + defaultTab?: string; + persistPageSize?: boolean | string; + defaultPageSize?: number; +}; + +export function DNSRecordsDataTable({ + columns, + data, + title, + addButtonText, + onAdd, + onRefresh, + isRefreshing, + defaultSort, + tabs, + defaultTab, + +}: DNSRecordsDataTableProps) { + const t = useTranslations(); + + const [activeTab, setActiveTab] = useState( + defaultTab || tabs?.[0]?.id || "" + ); + + // Apply tab filter to data + const filteredData = useMemo(() => { + if (!tabs || activeTab === "") { + return data; + } + + const activeTabFilter = tabs.find((tab) => tab.id === activeTab); + if (!activeTabFilter) { + return data; + } + + return data.filter(activeTabFilter.filterFn); + }, [data, tabs, activeTab]); + + const table = useReactTable({ + data: filteredData, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }); + + + + + return ( +
+ + +
+
+

DNS Records

+ Required +
+ {tabs && tabs.length > 0 && ( + + + {tabs.map((tab) => ( + + {tab.label} ( + {data.filter(tab.filterFn).length}) + + ))} + + + )} +
+
+ {onRefresh && ( + + )} + {onAdd && addButtonText && ( + + )} +
+
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results found. + + + )} + +
+
+
+
+ ); +} diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx index 8564acfa..f768a5c2 100644 --- a/src/components/DomainInfoCard.tsx +++ b/src/components/DomainInfoCard.tsx @@ -1,7 +1,6 @@ "use client"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { InfoIcon } from "lucide-react"; import { InfoSection, InfoSectionContent, @@ -24,15 +23,23 @@ import { } from "@app/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Input } from "./ui/input"; -import { CheckboxWithLabel } from "./ui/checkbox"; import { useForm } from "react-hook-form"; import z from "zod"; import { toASCII } from "punycode"; import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; import { Switch } from "./ui/switch"; +import DNSRecordsTable, { DNSRecordRow } from "./DNSrecordTable"; +import { useEffect, useState } from "react"; +import { createApiClient } from "@app/lib/api"; +import { useToast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/api"; +import { Badge } from "./ui/badge"; -type DomainInfoCardProps = {}; +type DomainInfoCardProps = { + orgId?: string; + domainId?: string; +}; // Helper functions for Unicode domain handling function toPunycode(domain: string): string { @@ -88,10 +95,16 @@ const certResolverOptions = [ ]; -export default function DomainInfoCard({ }: DomainInfoCardProps) { +export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) { const { domain, updateDomain } = useDomainContext(); const t = useTranslations(); const { env } = useEnvContext(); + const api = createApiClient(useEnvContext()); + const { toast } = useToast(); + + const [dnsRecords, setDnsRecords] = useState([]); + const [loadingRecords, setLoadingRecords] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); const form = useForm({ resolver: zodResolver(formSchema), @@ -103,6 +116,40 @@ export default function DomainInfoCard({ }: DomainInfoCardProps) { } }); + const fetchDNSRecords = async (showRefreshing = false) => { + if (showRefreshing) { + setIsRefreshing(true); + } else { + setLoadingRecords(true); + } + + try { + const response = await api.get<{ data: DNSRecordRow[] }>( + `/org/${orgId}/domain/${domainId}/dns-records` + ); + setDnsRecords(response.data.data); + } catch (error) { + // Only show error if records exist (not a 404) + const err = error as any; + if (err?.response?.status !== 404) { + toast({ + title: t("error"), + description: formatAxiosError(error), + variant: "destructive" + }); + } + } finally { + setLoadingRecords(false); + setIsRefreshing(false); + } + }; + + useEffect(() => { + if (domain.domainId) { + fetchDNSRecords(); + } + }, [domain.domainId]); + return ( <> @@ -126,8 +173,9 @@ export default function DomainInfoCard({ }: DomainInfoCardProps) { {domain.verified ? (
-
- {t("verified")} + + {t("verified")} +
) : (
@@ -141,12 +189,20 @@ export default function DomainInfoCard({ }: DomainInfoCardProps) { + {loadingRecords ? ( +
+ loading... +
+ ) : ( + + )} - - - - {/* Domain Settings */} - {/* Add condition later to only show when domain is wildcard */} + {/* Domain Settings */} + {/* Add condition later to only show when domain is wildcard */} @@ -257,4 +313,4 @@ export default function DomainInfoCard({ }: DomainInfoCardProps) { ); -} +} \ No newline at end of file