diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx
index 6d08636d1..9f9878967 100644
--- a/src/app/[orgId]/settings/domains/[domainId]/page.tsx
+++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx
@@ -1,16 +1,8 @@
-import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
-import DomainInfoCard from "@app/components/DomainInfoCard";
-import RestartDomainButton from "@app/components/RestartDomainButton";
+import DomainPageClient from "@app/components/DomainPageClient";
import { GetDomainResponse } from "@server/routers/domain/getDomain";
-import { pullEnv } from "@app/lib/pullEnv";
-import { getTranslations } from "next-intl/server";
-import RefreshButton from "@app/components/RefreshButton";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { GetDNSRecordsResponse } from "@server/routers/domain";
-import DNSRecordsTable from "@app/components/DNSRecordTable";
-import DomainCertForm from "@app/components/DomainCertForm";
-import { build } from "@server/build";
import type { Metadata } from "next";
export const metadata: Metadata = {
@@ -25,8 +17,6 @@ export default async function DomainSettingsPage({
params
}: DomainSettingsPageProps) {
const { domainId, orgId } = await params;
- const t = await getTranslations();
- const env = pullEnv();
let domain: GetDomainResponse | null = null;
try {
@@ -39,57 +29,27 @@ export default async function DomainSettingsPage({
return null;
}
- let dnsRecords;
+ let dnsRecords: GetDNSRecordsResponse | null = null;
try {
const response = await internal.get(
`/org/${orgId}/domain/${domainId}/dns-records`,
await authCookieHeader()
);
dnsRecords = response.data.data;
- } catch (error) {
+ } catch {
return null;
}
- if (!domain) {
+ if (!domain || !dnsRecords) {
return null;
}
return (
- <>
-
-
- {env.flags.usePangolinDns && domain.failed ? (
-
- ) : (
-
- )}
-
-
- {build != "oss" && env.flags.usePangolinDns ? (
-
- ) : null}
-
-
-
- {domain.type == "wildcard" && !domain.configManaged && (
-
- )}
-
- >
+
);
-}
+}
\ No newline at end of file
diff --git a/src/components/DomainPageClient.tsx b/src/components/DomainPageClient.tsx
new file mode 100644
index 000000000..31527c5b8
--- /dev/null
+++ b/src/components/DomainPageClient.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { domainQueries } from "@app/lib/queries";
+import { GetDomainResponse } from "@server/routers/domain/getDomain";
+import { GetDNSRecordsResponse } from "@server/routers/domain";
+import DomainInfoCard from "@app/components/DomainInfoCard";
+import DNSRecordsTable from "@app/components/DNSRecordTable";
+import RestartDomainButton from "@app/components/RestartDomainButton";
+import RefreshButton from "@app/components/RefreshButton";
+import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
+import DomainCertForm from "@app/components/DomainCertForm";
+import { build } from "@server/build";
+import { useEnvContext } from "@app/hooks/useEnvContext";
+import { useTranslations } from "next-intl";
+
+interface DomainPageClientProps {
+ initialDomain: GetDomainResponse;
+ initialDnsRecords: GetDNSRecordsResponse;
+ orgId: string;
+ domainId: string;
+}
+
+export default function DomainPageClient({
+ initialDomain,
+ initialDnsRecords,
+ orgId,
+ domainId
+}: DomainPageClientProps) {
+ const t = useTranslations();
+ const { env } = useEnvContext();
+
+ const { data: domain, refetch: refetchDomain } = useQuery({
+ ...domainQueries.getDomain({ orgId, domainId }),
+ initialData: initialDomain
+ });
+
+ const { data: dnsRecords, refetch: refetchDnsRecords } = useQuery({
+ ...domainQueries.getDNSRecords({ orgId, domainId }),
+ initialData: initialDnsRecords
+ });
+
+ const refetchAll = () => {
+ refetchDomain();
+ refetchDnsRecords();
+ };
+
+ return (
+ <>
+
+
+ {env.flags.usePangolinDns && domain.failed ? (
+
+ ) : (
+
+ )}
+
+
+ {build !== "oss" && env.flags.usePangolinDns ? (
+
+ ) : null}
+
+ ({
+ ...r,
+ id: String(r.id)
+ }))}
+ type={domain.type}
+ />
+
+ {domain.type === "wildcard" && !domain.configManaged && (
+
+ )}
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/DomainsTable.tsx b/src/components/DomainsTable.tsx
index f5cb1ae74..2c3abeb1a 100644
--- a/src/components/DomainsTable.tsx
+++ b/src/components/DomainsTable.tsx
@@ -10,13 +10,12 @@ import {
MoreHorizontal,
RefreshCw
} from "lucide-react";
-import { useState } from "react";
+import { useMemo, useState } from "react";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { Badge } from "@app/components/ui/badge";
-import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import CreateDomainForm from "@app/components/CreateDomainForm";
import { useToast } from "@app/hooks/useToast";
@@ -34,6 +33,10 @@ import {
TooltipTrigger
} from "./ui/tooltip";
import Link from "next/link";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { orgQueries } from "@app/lib/queries";
+import { toUnicode } from "punycode";
+import { durationToMs } from "@app/lib/durationToMs";
export type DomainRow = {
domainId: string;
@@ -59,32 +62,32 @@ export default function DomainsTable({ domains, orgId }: Props) {
const [selectedDomain, setSelectedDomain] = useState(
null
);
- const [isRefreshing, setIsRefreshing] = useState(false);
const [restartingDomains, setRestartingDomains] = useState>(
new Set()
);
const env = useEnvContext();
const api = createApiClient(env);
- const router = useRouter();
const t = useTranslations();
const { toast } = useToast();
const { org } = useOrgContext();
+ const queryClient = useQueryClient();
- const refreshData = async () => {
- setIsRefreshing(true);
- try {
- await new Promise((resolve) => setTimeout(resolve, 200));
- router.refresh();
- } catch (error) {
- toast({
- title: t("error"),
- description: t("refreshError"),
- variant: "destructive"
- });
- } finally {
- setIsRefreshing(false);
- }
- };
+ const { data: rawDomains, isRefetching, refetch } = useQuery({
+ ...orgQueries.domains({ orgId }),
+ initialData: domains as any,
+ refetchInterval: durationToMs(10, "seconds")
+ });
+
+ const tableData = useMemo(
+ () =>
+ (rawDomains ?? []).map((d) => ({
+ ...d,
+ baseDomain: toUnicode(d.baseDomain),
+ type: d.type ?? "",
+ errorMessage: d.errorMessage ?? null
+ } as DomainRow)),
+ [rawDomains]
+ );
const deleteDomain = async (domainId: string) => {
try {
@@ -94,7 +97,7 @@ export default function DomainsTable({ domains, orgId }: Props) {
description: t("domainDeletedDescription")
});
setIsDeleteModalOpen(false);
- refreshData();
+ refetch();
} catch (e) {
toast({
title: t("error"),
@@ -114,7 +117,7 @@ export default function DomainsTable({ domains, orgId }: Props) {
fallback: "Domain verification restarted successfully"
})
});
- refreshData();
+ refetch();
} catch (e) {
toast({
title: t("error"),
@@ -361,16 +364,16 @@ export default function DomainsTable({ domains, orgId }: Props) {
open={isCreateModalOpen}
setOpen={setIsCreateModalOpen}
onCreated={(domain) => {
- refreshData();
+ refetch();
}}
/>
setIsCreateModalOpen(true)}
- onRefresh={refreshData}
- isRefreshing={isRefreshing}
+ onRefresh={refetch}
+ isRefreshing={isRefetching}
/>
>
);
diff --git a/src/components/RefreshButton.tsx b/src/components/RefreshButton.tsx
index 3ba7d4f32..67799546f 100644
--- a/src/components/RefreshButton.tsx
+++ b/src/components/RefreshButton.tsx
@@ -7,7 +7,11 @@ import { Button } from "@app/components/ui/button";
import { useTranslations } from "next-intl";
import { toast } from "@app/hooks/useToast";
-export default function RefreshButton() {
+interface RefreshButtonProps {
+ onRefresh?: () => void;
+}
+
+export default function RefreshButton({ onRefresh }: RefreshButtonProps = {}) {
const router = useRouter();
const [isRefreshing, setIsRefreshing] = useState(false);
const t = useTranslations();
@@ -16,7 +20,11 @@ export default function RefreshButton() {
setIsRefreshing(true);
try {
await new Promise((resolve) => setTimeout(resolve, 200));
- router.refresh();
+ if (onRefresh) {
+ onRefresh();
+ } else {
+ router.refresh();
+ }
} catch {
toast({
title: t("error"),
diff --git a/src/components/RestartDomainButton.tsx b/src/components/RestartDomainButton.tsx
index 670f4fa2c..5501ad1ed 100644
--- a/src/components/RestartDomainButton.tsx
+++ b/src/components/RestartDomainButton.tsx
@@ -12,11 +12,13 @@ import { useTranslations } from "next-intl";
interface RestartDomainButtonProps {
orgId: string;
domainId: string;
+ onSuccess?: () => void;
}
export default function RestartDomainButton({
orgId,
- domainId
+ domainId,
+ onSuccess
}: RestartDomainButtonProps) {
const router = useRouter();
const api = createApiClient(useEnvContext());
@@ -35,7 +37,11 @@ export default function RestartDomainButton({
});
// Wait a bit before refreshing to allow the restart to take effect
await new Promise((resolve) => setTimeout(resolve, 200));
- router.refresh();
+ if (onSuccess) {
+ onSuccess();
+ } else {
+ router.refresh();
+ }
} catch (e) {
toast({
title: t("error"),
diff --git a/src/lib/queries.ts b/src/lib/queries.ts
index 2fd34e8ac..7a22639fe 100644
--- a/src/lib/queries.ts
+++ b/src/lib/queries.ts
@@ -1,7 +1,8 @@
import { build } from "@server/build";
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
import type { ListClientsResponse } from "@server/routers/client";
-import type { ListDomainsResponse } from "@server/routers/domain";
+import type { ListDomainsResponse, GetDNSRecordsResponse } from "@server/routers/domain";
+import type { GetDomainResponse } from "@server/routers/domain/getDomain";
import type {
GetResourceWhitelistResponse,
ListResourceNamesResponse,
@@ -472,3 +473,49 @@ export const approvalQueries = {
}
})
};
+
+export const domainQueries = {
+ getDomain: ({
+ orgId,
+ domainId
+ }: {
+ orgId: string;
+ domainId: string;
+ }) =>
+ queryOptions({
+ queryKey: ["ORG", orgId, "DOMAIN", domainId] as const,
+ queryFn: async ({ signal, meta }) => {
+ const res = await meta!.api.get<
+ AxiosResponse
+ >(`/org/${orgId}/domain/${domainId}`, { signal });
+ return res.data.data;
+ },
+ refetchInterval: durationToMs(10, "seconds")
+ }),
+ getDNSRecords: ({
+ orgId,
+ domainId
+ }: {
+ orgId: string;
+ domainId: string;
+ }) =>
+ queryOptions({
+ queryKey: [
+ "ORG",
+ orgId,
+ "DOMAIN",
+ domainId,
+ "DNS_RECORDS"
+ ] as const,
+ queryFn: async ({ signal, meta }) => {
+ const res = await meta!.api.get<
+ AxiosResponse
+ >(
+ `/org/${orgId}/domain/${domainId}/dns-records`,
+ { signal }
+ );
+ return res.data.data;
+ },
+ refetchInterval: durationToMs(10, "seconds")
+ })
+};