mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-28 08:46:59 +00:00
Refresh domains for latest status
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
93
src/components/DomainPageClient.tsx
Normal file
93
src/components/DomainPageClient.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user