mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-29 15:09:07 +01:00
Split into tabs
This commit is contained in:
@@ -3741,6 +3741,11 @@
|
||||
"TTL": "TTL",
|
||||
"howToAddRecords": "How to Add Records",
|
||||
"dnsRecord": "DNS Records",
|
||||
"domainTabSetup": "Setup",
|
||||
"domainTabRecords": "Records",
|
||||
"domainSetupRecordsDescription": "Create these records at your domain registrar or current DNS provider. Pangolin uses them to verify the domain and route its traffic.",
|
||||
"domainRecordsInfoTitle": "Records served by Pangolin",
|
||||
"domainRecordsInfoDescription": "Pangolin's nameservers answer DNS queries for {domain}. Records you add here are served directly from Pangolin, so you don't need to create them at your registrar.",
|
||||
"customDnsRecords": "Custom DNS Records",
|
||||
"customDnsRecordsAdd": "Add Record",
|
||||
"customDnsRecordsSearch": "Search records...",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { GetDomainResponse } from "@server/routers/domain/getDomain";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import DomainHeader from "@app/components/DomainHeader";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import { supportsCustomDnsRecords } from "@app/lib/customDnsRecords";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Domain"
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
interface DomainLayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ domainId: string; orgId: string }>;
|
||||
}
|
||||
|
||||
export default async function DomainLayout(props: DomainLayoutProps) {
|
||||
const { domainId, orgId } = await props.params;
|
||||
const t = await getTranslations();
|
||||
const env = pullEnv();
|
||||
|
||||
let domain: GetDomainResponse | null = null;
|
||||
try {
|
||||
const res = await internal.get<AxiosResponse<GetDomainResponse>>(
|
||||
`/org/${orgId}/domain/${domainId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
domain = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${orgId}/settings/domains`);
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
redirect(`/${orgId}/settings/domains`);
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t("domainTabSetup"),
|
||||
href: `/{orgId}/settings/domains/{domainId}/setup`
|
||||
}
|
||||
];
|
||||
|
||||
if (supportsCustomDnsRecords(env, domain.type)) {
|
||||
navItems.push({
|
||||
title: t("domainTabRecords"),
|
||||
href: `/{orgId}/settings/domains/{domainId}/records`
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DomainHeader initialDomain={domain} orgId={orgId} />
|
||||
<HorizontalTabs items={navItems}>{props.children}</HorizontalTabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +1,8 @@
|
||||
import DomainPageClient from "@app/components/DomainPageClient";
|
||||
import { GetDomainResponse } from "@server/routers/domain/getDomain";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { GetDNSRecordsResponse } from "@server/routers/domain";
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Domain"
|
||||
};
|
||||
|
||||
interface DomainSettingsPageProps {
|
||||
export default async function DomainPage(props: {
|
||||
params: Promise<{ domainId: string; orgId: string }>;
|
||||
}) {
|
||||
const { domainId, orgId } = await props.params;
|
||||
redirect(`/${orgId}/settings/domains/${domainId}/setup`);
|
||||
}
|
||||
|
||||
export default async function DomainSettingsPage({
|
||||
params
|
||||
}: DomainSettingsPageProps) {
|
||||
const { domainId, orgId } = await params;
|
||||
|
||||
let domain: GetDomainResponse | null = null;
|
||||
try {
|
||||
const res = await internal.get(
|
||||
`/org/${orgId}/domain/${domainId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
domain = res.data.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
let dnsRecords: GetDNSRecordsResponse | null = null;
|
||||
try {
|
||||
const response = await internal.get(
|
||||
`/org/${orgId}/domain/${domainId}/dns-records`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
dnsRecords = response.data.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!domain || !dnsRecords) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DomainPageClient
|
||||
initialDomain={domain}
|
||||
initialDnsRecords={dnsRecords}
|
||||
orgId={orgId}
|
||||
domainId={domainId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import DomainRecordsClient from "@app/components/DomainRecordsClient";
|
||||
import { GetDomainResponse } from "@server/routers/domain/getDomain";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import { supportsCustomDnsRecords } from "@app/lib/customDnsRecords";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
interface DomainRecordsPageProps {
|
||||
params: Promise<{ domainId: string; orgId: string }>;
|
||||
}
|
||||
|
||||
export default async function DomainRecordsPage({
|
||||
params
|
||||
}: DomainRecordsPageProps) {
|
||||
const { domainId, orgId } = await params;
|
||||
|
||||
let domain: GetDomainResponse | null = null;
|
||||
try {
|
||||
const res = await internal.get(
|
||||
`/org/${orgId}/domain/${domainId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
domain = res.data.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!supportsCustomDnsRecords(pullEnv(), domain.type)) {
|
||||
redirect(`/${orgId}/settings/domains/${domainId}/setup`);
|
||||
}
|
||||
|
||||
return (
|
||||
<DomainRecordsClient
|
||||
initialDomain={domain}
|
||||
orgId={orgId}
|
||||
domainId={domainId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import DomainSetupClient from "@app/components/DomainSetupClient";
|
||||
import { GetDomainResponse } from "@server/routers/domain/getDomain";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { GetDNSRecordsResponse } from "@server/routers/domain";
|
||||
|
||||
interface DomainSetupPageProps {
|
||||
params: Promise<{ domainId: string; orgId: string }>;
|
||||
}
|
||||
|
||||
export default async function DomainSetupPage({
|
||||
params
|
||||
}: DomainSetupPageProps) {
|
||||
const { domainId, orgId } = await params;
|
||||
|
||||
let domain: GetDomainResponse | null = null;
|
||||
try {
|
||||
const res = await internal.get(
|
||||
`/org/${orgId}/domain/${domainId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
domain = res.data.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
let dnsRecords: GetDNSRecordsResponse | null = null;
|
||||
try {
|
||||
const response = await internal.get(
|
||||
`/org/${orgId}/domain/${domainId}/dns-records`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
dnsRecords = response.data.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!domain || !dnsRecords) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DomainSetupClient
|
||||
initialDomain={domain}
|
||||
initialDnsRecords={dnsRecords}
|
||||
orgId={orgId}
|
||||
domainId={domainId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -106,9 +106,18 @@ export function DNSRecordsDataTable<TData, TValue>({
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 pb-4">
|
||||
<div className="flex flex-row space-y-3 w-full sm:mr-2 gap-2 justify-between">
|
||||
<div className="relative w-full sm:max-w-sm flex flex-row gap-4 items-center">
|
||||
<h1 className="font-semibold">{t("dnsRecord")}</h1>
|
||||
<Badge variant="secondary">{t("required")}</Badge>
|
||||
<div className="relative w-full space-y-1">
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
<h1 className="font-semibold">
|
||||
{t("dnsRecord")}
|
||||
</h1>
|
||||
<Badge variant="secondary">
|
||||
{t("required")}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("domainSetupRecordsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="https://docs.pangolin.net/manage/domains"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { domainQueries } from "@app/lib/queries";
|
||||
import { GetDomainResponse } from "@server/routers/domain/getDomain";
|
||||
import RestartDomainButton from "@app/components/RestartDomainButton";
|
||||
import RefreshButton from "@app/components/RefreshButton";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Lock } from "lucide-react";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
|
||||
interface DomainHeaderProps {
|
||||
initialDomain: GetDomainResponse;
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export default function DomainHeader({
|
||||
initialDomain,
|
||||
orgId
|
||||
}: DomainHeaderProps) {
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const queryClient = useQueryClient();
|
||||
const domainId = initialDomain.domainId;
|
||||
|
||||
const { data: domain } = useQuery({
|
||||
...domainQueries.getDomain({ orgId, domainId }),
|
||||
initialData: initialDomain
|
||||
});
|
||||
|
||||
// Every domain query shares this key prefix, so this refreshes whichever
|
||||
// tab is showing
|
||||
const refetchAll = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["ORG", orgId, "DOMAIN", domainId]
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<SettingsSectionTitle
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
{domain.baseDomain}
|
||||
{domain.configManaged && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 text-sm font-normal"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
{t("configManaged", {
|
||||
fallback: "Config Managed"
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
description={t("domainSettingDescription")}
|
||||
/>
|
||||
{env.flags.usePangolinDns && domain.failed ? (
|
||||
<RestartDomainButton
|
||||
orgId={orgId}
|
||||
domainId={domain.domainId}
|
||||
onSuccess={refetchAll}
|
||||
/>
|
||||
) : (
|
||||
<RefreshButton onRefresh={refetchAll} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
"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 CustomDnsRecordsTable from "@app/components/CustomDnsRecordsTable";
|
||||
import { build } from "@server/build";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Lock } from "lucide-react";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
|
||||
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={
|
||||
<span className="flex items-center gap-2">
|
||||
{domain.baseDomain}
|
||||
{domain.configManaged && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 text-sm font-normal"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
{t("configManaged", {
|
||||
fallback: "Config Managed"
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
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}
|
||||
/>
|
||||
|
||||
{build !== "oss" &&
|
||||
env.flags.usePangolinDns &&
|
||||
!env.flags.disableEnterpriseFeatures &&
|
||||
domain.type === "ns" && (
|
||||
<CustomDnsRecordsTable
|
||||
orgId={orgId}
|
||||
domainId={domain.domainId}
|
||||
baseDomain={domain.baseDomain}
|
||||
verified={domain.verified}
|
||||
/>
|
||||
)}
|
||||
|
||||
{domain.type === "wildcard" && !domain.configManaged && (
|
||||
<DomainCertForm
|
||||
orgId={orgId}
|
||||
domainId={domain.domainId}
|
||||
domain={domain}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { domainQueries } from "@app/lib/queries";
|
||||
import { GetDomainResponse } from "@server/routers/domain/getDomain";
|
||||
import CustomDnsRecordsTable from "@app/components/CustomDnsRecordsTable";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
|
||||
interface DomainRecordsClientProps {
|
||||
initialDomain: GetDomainResponse;
|
||||
orgId: string;
|
||||
domainId: string;
|
||||
}
|
||||
|
||||
export default function DomainRecordsClient({
|
||||
initialDomain,
|
||||
orgId,
|
||||
domainId
|
||||
}: DomainRecordsClientProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const { data: domain } = useQuery({
|
||||
...domainQueries.getDomain({ orgId, domainId }),
|
||||
initialData: initialDomain
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert variant="info">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle>{t("domainRecordsInfoTitle")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("domainRecordsInfoDescription", {
|
||||
domain: domain.baseDomain
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<CustomDnsRecordsTable
|
||||
orgId={orgId}
|
||||
domainId={domain.domainId}
|
||||
baseDomain={domain.baseDomain}
|
||||
verified={domain.verified}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
"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 DomainCertForm from "@app/components/DomainCertForm";
|
||||
import { build } from "@server/build";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
interface DomainSetupClientProps {
|
||||
initialDomain: GetDomainResponse;
|
||||
initialDnsRecords: GetDNSRecordsResponse;
|
||||
orgId: string;
|
||||
domainId: string;
|
||||
}
|
||||
|
||||
export default function DomainSetupClient({
|
||||
initialDomain,
|
||||
initialDnsRecords,
|
||||
orgId,
|
||||
domainId
|
||||
}: DomainSetupClientProps) {
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const { data: domain } = useQuery({
|
||||
...domainQueries.getDomain({ orgId, domainId }),
|
||||
initialData: initialDomain
|
||||
});
|
||||
|
||||
const { data: dnsRecords } = useQuery({
|
||||
...domainQueries.getDNSRecords({ orgId, domainId }),
|
||||
initialData: initialDnsRecords
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export function HorizontalTabs({
|
||||
.replace("{orgId}", params.orgId as string)
|
||||
.replace("{resourceId}", params.resourceId as string)
|
||||
.replace("{niceId}", params.niceId as string)
|
||||
.replace("{domainId}", params.domainId as string)
|
||||
.replace("{userId}", params.userId as string)
|
||||
.replace("{clientId}", params.clientId as string)
|
||||
.replace("{apiKeyId}", params.apiKeyId as string)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { build } from "@server/build";
|
||||
import { Env } from "./types/env";
|
||||
|
||||
// Custom records are only served by Pangolin's own nameservers, so they only
|
||||
// apply to NS-delegated domains on deployments running Pangolin DNS
|
||||
export function supportsCustomDnsRecords(
|
||||
env: Env,
|
||||
domainType: string | null
|
||||
): boolean {
|
||||
return (
|
||||
build !== "oss" &&
|
||||
env.flags.usePangolinDns &&
|
||||
!env.flags.disableEnterpriseFeatures &&
|
||||
domainType === "ns"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user