mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-29 01:08:13 +00:00
Refresh domains for latest status
This commit is contained in:
@@ -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}
|
orgId={orgId}
|
||||||
description={t("domainSettingDescription")}
|
domainId={domainId}
|
||||||
/>
|
/>
|
||||||
{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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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,
|
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({
|
const tableData = useMemo(
|
||||||
title: t("error"),
|
() =>
|
||||||
description: t("refreshError"),
|
(rawDomains ?? []).map((d) => ({
|
||||||
variant: "destructive"
|
...d,
|
||||||
});
|
baseDomain: toUnicode(d.baseDomain),
|
||||||
} finally {
|
type: d.type ?? "",
|
||||||
setIsRefreshing(false);
|
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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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));
|
||||||
router.refresh();
|
if (onRefresh) {
|
||||||
|
onRefresh();
|
||||||
|
} else {
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: t("error"),
|
title: t("error"),
|
||||||
|
|||||||
@@ -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));
|
||||||
router.refresh();
|
if (onSuccess) {
|
||||||
|
onSuccess();
|
||||||
|
} else {
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
title: t("error"),
|
title: t("error"),
|
||||||
|
|||||||
@@ -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")
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user