From 9a6408d28cd8a21ae3df14e0bd942dddc6e90d69 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 20 Apr 2026 14:57:24 -0700 Subject: [PATCH] Refresh domains for latest status --- .../settings/domains/[domainId]/page.tsx | 62 +++---------- src/components/DomainPageClient.tsx | 93 +++++++++++++++++++ src/components/DomainsTable.tsx | 53 ++++++----- src/components/RefreshButton.tsx | 12 ++- src/components/RestartDomainButton.tsx | 10 +- src/lib/queries.ts | 49 +++++++++- 6 files changed, 198 insertions(+), 81 deletions(-) create mode 100644 src/components/DomainPageClient.tsx 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") + }) +};