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 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 (
<>
<div className="flex justify-between">
<SettingsSectionTitle
title={domain.baseDomain}
description={t("domainSettingDescription")}
/>
{env.flags.usePangolinDns && domain.failed ? (
<RestartDomainButton
orgId={orgId}
domainId={domain.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>
</>
<DomainPageClient
initialDomain={domain}
initialDnsRecords={dnsRecords}
orgId={orgId}
domainId={domainId}
/>
);
}
}

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,
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<DomainRow | null>(
null
);
const [isRefreshing, setIsRefreshing] = useState(false);
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(
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();
}}
/>
<DomainsDataTable
columns={columns}
data={domains}
data={tableData}
onAdd={() => setIsCreateModalOpen(true)}
onRefresh={refreshData}
isRefreshing={isRefreshing}
onRefresh={refetch}
isRefreshing={isRefetching}
/>
</>
);

View File

@@ -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"),

View File

@@ -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"),

View File

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