Refresh domains for latest status

This commit is contained in:
Owen
2026-04-20 14:57:24 -07:00
parent 2dad97cb6b
commit 9a6408d28c
6 changed files with 198 additions and 81 deletions

View File

@@ -1,16 +1,8 @@
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import DomainPageClient from "@app/components/DomainPageClient";
import DomainInfoCard from "@app/components/DomainInfoCard";
import RestartDomainButton from "@app/components/RestartDomainButton";
import { GetDomainResponse } from "@server/routers/domain/getDomain"; 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 { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { GetDNSRecordsResponse } from "@server/routers/domain"; 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"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -25,8 +17,6 @@ export default async function DomainSettingsPage({
params params
}: DomainSettingsPageProps) { }: DomainSettingsPageProps) {
const { domainId, orgId } = await params; const { domainId, orgId } = await params;
const t = await getTranslations();
const env = pullEnv();
let domain: GetDomainResponse | null = null; let domain: GetDomainResponse | null = null;
try { try {
@@ -39,57 +29,27 @@ export default async function DomainSettingsPage({
return null; return null;
} }
let dnsRecords; let dnsRecords: GetDNSRecordsResponse | null = null;
try { try {
const response = await internal.get( const response = await internal.get(
`/org/${orgId}/domain/${domainId}/dns-records`, `/org/${orgId}/domain/${domainId}/dns-records`,
await authCookieHeader() await authCookieHeader()
); );
dnsRecords = response.data.data; dnsRecords = response.data.data;
} catch (error) { } catch {
return null; return null;
} }
if (!domain) { if (!domain || !dnsRecords) {
return null; return null;
} }
return ( return (
<> <DomainPageClient
<div className="flex justify-between"> initialDomain={domain}
<SettingsSectionTitle initialDnsRecords={dnsRecords}
title={domain.baseDomain}
description={t("domainSettingDescription")}
/>
{env.flags.usePangolinDns && domain.failed ? (
<RestartDomainButton
orgId={orgId} orgId={orgId}
domainId={domain.domainId} domainId={domainId}
/> />
) : (
<RefreshButton />
)}
</div>
<div className="space-y-6">
{build != "oss" && env.flags.usePangolinDns ? (
<DomainInfoCard
failed={domain.failed}
verified={domain.verified}
type={domain.type}
errorMessage={domain.errorMessage}
/>
) : null}
<DNSRecordsTable records={dnsRecords} type={domain.type} />
{domain.type == "wildcard" && !domain.configManaged && (
<DomainCertForm
orgId={orgId}
domainId={domain.domainId}
domain={domain}
/>
)}
</div>
</>
); );
} }

View File

@@ -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 (
<>
<div className="flex justify-between">
<SettingsSectionTitle
title={domain.baseDomain}
description={t("domainSettingDescription")}
/>
{env.flags.usePangolinDns && domain.failed ? (
<RestartDomainButton
orgId={orgId}
domainId={domain.domainId}
onSuccess={refetchAll}
/>
) : (
<RefreshButton onRefresh={refetchAll} />
)}
</div>
<div className="space-y-6">
{build !== "oss" && env.flags.usePangolinDns ? (
<DomainInfoCard
failed={domain.failed}
verified={domain.verified}
type={domain.type}
errorMessage={domain.errorMessage}
/>
) : null}
<DNSRecordsTable
records={dnsRecords.map((r) => ({
...r,
id: String(r.id)
}))}
type={domain.type}
/>
{domain.type === "wildcard" && !domain.configManaged && (
<DomainCertForm
orgId={orgId}
domainId={domain.domainId}
domain={domain}
/>
)}
</div>
</>
);
}

View File

@@ -10,13 +10,12 @@ import {
MoreHorizontal, MoreHorizontal,
RefreshCw RefreshCw
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useMemo, useState } from "react";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { formatAxiosError } from "@app/lib/api"; import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { Badge } from "@app/components/ui/badge"; import { Badge } from "@app/components/ui/badge";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import CreateDomainForm from "@app/components/CreateDomainForm"; import CreateDomainForm from "@app/components/CreateDomainForm";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
@@ -34,6 +33,10 @@ import {
TooltipTrigger TooltipTrigger
} from "./ui/tooltip"; } from "./ui/tooltip";
import Link from "next/link"; 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 = { export type DomainRow = {
domainId: string; domainId: string;
@@ -59,32 +62,32 @@ export default function DomainsTable({ domains, orgId }: Props) {
const [selectedDomain, setSelectedDomain] = useState<DomainRow | null>( const [selectedDomain, setSelectedDomain] = useState<DomainRow | null>(
null null
); );
const [isRefreshing, setIsRefreshing] = useState(false);
const [restartingDomains, setRestartingDomains] = useState<Set<string>>( const [restartingDomains, setRestartingDomains] = useState<Set<string>>(
new Set() new Set()
); );
const env = useEnvContext(); const env = useEnvContext();
const api = createApiClient(env); const api = createApiClient(env);
const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const { toast } = useToast(); const { toast } = useToast();
const { org } = useOrgContext(); const { org } = useOrgContext();
const queryClient = useQueryClient();
const refreshData = async () => { const { data: rawDomains, isRefetching, refetch } = useQuery({
setIsRefreshing(true); ...orgQueries.domains({ orgId }),
try { initialData: domains as any,
await new Promise((resolve) => setTimeout(resolve, 200)); refetchInterval: durationToMs(10, "seconds")
router.refresh();
} catch (error) {
toast({
title: t("error"),
description: t("refreshError"),
variant: "destructive"
}); });
} finally {
setIsRefreshing(false); 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) => { const deleteDomain = async (domainId: string) => {
try { try {
@@ -94,7 +97,7 @@ export default function DomainsTable({ domains, orgId }: Props) {
description: t("domainDeletedDescription") description: t("domainDeletedDescription")
}); });
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
refreshData(); refetch();
} catch (e) { } catch (e) {
toast({ toast({
title: t("error"), title: t("error"),
@@ -114,7 +117,7 @@ export default function DomainsTable({ domains, orgId }: Props) {
fallback: "Domain verification restarted successfully" fallback: "Domain verification restarted successfully"
}) })
}); });
refreshData(); refetch();
} catch (e) { } catch (e) {
toast({ toast({
title: t("error"), title: t("error"),
@@ -361,16 +364,16 @@ export default function DomainsTable({ domains, orgId }: Props) {
open={isCreateModalOpen} open={isCreateModalOpen}
setOpen={setIsCreateModalOpen} setOpen={setIsCreateModalOpen}
onCreated={(domain) => { onCreated={(domain) => {
refreshData(); refetch();
}} }}
/> />
<DomainsDataTable <DomainsDataTable
columns={columns} columns={columns}
data={domains} data={tableData}
onAdd={() => setIsCreateModalOpen(true)} onAdd={() => setIsCreateModalOpen(true)}
onRefresh={refreshData} onRefresh={refetch}
isRefreshing={isRefreshing} isRefreshing={isRefetching}
/> />
</> </>
); );

View File

@@ -7,7 +7,11 @@ import { Button } from "@app/components/ui/button";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
export default function RefreshButton() { interface RefreshButtonProps {
onRefresh?: () => void;
}
export default function RefreshButton({ onRefresh }: RefreshButtonProps = {}) {
const router = useRouter(); const router = useRouter();
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const t = useTranslations(); const t = useTranslations();
@@ -16,7 +20,11 @@ export default function RefreshButton() {
setIsRefreshing(true); setIsRefreshing(true);
try { try {
await new Promise((resolve) => setTimeout(resolve, 200)); await new Promise((resolve) => setTimeout(resolve, 200));
if (onRefresh) {
onRefresh();
} else {
router.refresh(); router.refresh();
}
} catch { } catch {
toast({ toast({
title: t("error"), title: t("error"),

View File

@@ -12,11 +12,13 @@ import { useTranslations } from "next-intl";
interface RestartDomainButtonProps { interface RestartDomainButtonProps {
orgId: string; orgId: string;
domainId: string; domainId: string;
onSuccess?: () => void;
} }
export default function RestartDomainButton({ export default function RestartDomainButton({
orgId, orgId,
domainId domainId,
onSuccess
}: RestartDomainButtonProps) { }: RestartDomainButtonProps) {
const router = useRouter(); const router = useRouter();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
@@ -35,7 +37,11 @@ export default function RestartDomainButton({
}); });
// Wait a bit before refreshing to allow the restart to take effect // Wait a bit before refreshing to allow the restart to take effect
await new Promise((resolve) => setTimeout(resolve, 200)); await new Promise((resolve) => setTimeout(resolve, 200));
if (onSuccess) {
onSuccess();
} else {
router.refresh(); router.refresh();
}
} catch (e) { } catch (e) {
toast({ toast({
title: t("error"), title: t("error"),

View File

@@ -1,7 +1,8 @@
import { build } from "@server/build"; import { build } from "@server/build";
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs"; import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
import type { ListClientsResponse } from "@server/routers/client"; 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 { import type {
GetResourceWhitelistResponse, GetResourceWhitelistResponse,
ListResourceNamesResponse, 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<GetDomainResponse>
>(`/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<GetDNSRecordsResponse>
>(
`/org/${orgId}/domain/${domainId}/dns-records`,
{ signal }
);
return res.data.data;
},
refetchInterval: durationToMs(10, "seconds")
})
};