add DNS Records table

This commit is contained in:
Pallavi Kumari
2025-10-18 01:12:02 +05:30
parent 8fdf120ec2
commit c29ba9bb5f
6 changed files with 426 additions and 17 deletions

View File

@@ -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"
}

View File

@@ -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
});
}
}

View File

@@ -42,7 +42,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
<DomainProvider domain={domain}>
<div className="space-y-6">
<DomainInfoCard />
<DomainInfoCard orgId={params.orgId} domainId={params.domainId} />
</div>
</DomainProvider>
</>

View File

@@ -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<DNSRecordRow>[] = [
{
accessorKey: "baseDomain",
header: ({ column }) => {
return (
<Button
variant="ghost"
>
{t("recordName", { fallback: "Record name" })}
</Button>
);
},
cell: ({ row }) => {
const baseDomain = row.original.baseDomain;
return (
<div className="font-mono text-sm">
{baseDomain || "-"}
</div>
);
}
},
{
accessorKey: "recordType",
header: ({ column }) => {
return (
<Button
variant="ghost"
>
{t("type")}
</Button>
);
},
cell: ({ row }) => {
const type = row.original.recordType;
return (
<div className="">
{type}
</div>
);
}
},
{
accessorKey: "ttl",
header: ({ column }) => {
return (
<Button
variant="ghost"
>
{t("TTL")}
</Button>
);
},
cell: ({ row }) => {
return (
<div>
{t("auto")}
</div>
);
}
},
{
accessorKey: "value",
header: () => {
return <div>{t("value")}</div>;
},
cell: ({ row }) => {
const value = row.original.value;
return (
<div className="flex items-center gap-2">
<div className="font-mono text-sm truncate max-w-md">
{value}
</div>
</div>
);
}
},
{
accessorKey: "verified",
header: ({ column }) => {
return (
<Button
variant="ghost"
>
{t("status")}
</Button>
);
},
cell: ({ row }) => {
const verified = row.original.verified;
return (
verified ? (
<Badge variant="green">{t("verified")}</Badge>
) : (
<Badge variant="secondary">{t("unverified")}</Badge>
)
);
}
}
];
return (
<DNSRecordsDataTable
columns={columns}
data={records}
isRefreshing={isRefreshing}
/>
);
}

View File

@@ -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<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
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<TData, TValue>({
columns,
data,
title,
addButtonText,
onAdd,
onRefresh,
isRefreshing,
defaultSort,
tabs,
defaultTab,
}: DNSRecordsDataTableProps<TData, TValue>) {
const t = useTranslations();
const [activeTab, setActiveTab] = useState<string>(
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 (
<div className="container mx-auto max-w-12xl">
<Card>
<CardHeader className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 pb-4">
<div className="flex flex-row space-y-3 w-full sm:mr-2 gap-2">
<div className="relative w-full sm:max-w-sm flex flex-row gap-10 items-center">
<h1 className="">DNS Records</h1>
<Badge variant="secondary">Required</Badge>
</div>
{tabs && tabs.length > 0 && (
<Tabs
value={activeTab}
className="w-full"
>
<TabsList>
{tabs.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
>
{tab.label} (
{data.filter(tab.filterFn).length})
</TabsTrigger>
))}
</TabsList>
</Tabs>
)}
</div>
<div className="flex items-center gap-2 sm:justify-end">
{onRefresh && (
<Button
variant="outline"
onClick={onRefresh}
disabled={isRefreshing}
>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>
{t("refresh")}
</Button>
)}
{onAdd && addButtonText && (
<Button onClick={onAdd}>
<Plus className="mr-2 h-4 w-4" />
{addButtonText}
</Button>
)}
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef
.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={
row.getIsSelected() && "selected"
}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@@ -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<DNSRecordRow[]>([]);
const [loadingRecords, setLoadingRecords] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const form = useForm<FormValues>({
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) {
<InfoSectionContent>
{domain.verified ? (
<div className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>{t("verified")}</span>
<Badge variant="green">
{t("verified")}
</Badge>
</div>
) : (
<div className="text-neutral-500 flex items-center space-x-2">
@@ -141,12 +189,20 @@ export default function DomainInfoCard({ }: DomainInfoCardProps) {
</AlertDescription>
</Alert>
{loadingRecords ? (
<div className="space-y-4">
loading...
</div>
) : (
<DNSRecordsTable
domainId={domain.domainId}
records={dnsRecords}
isRefreshing={isRefreshing}
/>
)}
{/* Domain Settings */}
{/* Add condition later to only show when domain is wildcard */}
{/* Domain Settings */}
{/* Add condition later to only show when domain is wildcard */}
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
@@ -257,4 +313,4 @@ export default function DomainInfoCard({ }: DomainInfoCardProps) {
</SettingsContainer >
</>
);
}
}