mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-31 23:29:08 +00:00
Various fixes for rc
This commit is contained in:
@@ -12,93 +12,98 @@ import { useDomain } from "@app/contexts/domainContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function DomainSettingsPage() {
|
||||
const { domain, orgId } = useDomain();
|
||||
const router = useRouter();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(new Set());
|
||||
const t = useTranslations();
|
||||
const { domain, orgId } = useDomain();
|
||||
const router = useRouter();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const refreshData = async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
router.refresh();
|
||||
} catch {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("refreshError"),
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
const refreshData = async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
router.refresh();
|
||||
} catch {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("refreshError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restartDomain = async (domainId: string) => {
|
||||
setRestartingDomains((prev) => new Set(prev).add(domainId));
|
||||
try {
|
||||
await api.post(`/org/${orgId}/domain/${domainId}/restart`);
|
||||
toast({
|
||||
title: t("success"),
|
||||
description: t("domainRestartedDescription", {
|
||||
fallback: "Domain verification restarted successfully"
|
||||
})
|
||||
});
|
||||
refreshData();
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setRestartingDomains((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(domainId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!domain) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const restartDomain = async (domainId: string) => {
|
||||
setRestartingDomains((prev) => new Set(prev).add(domainId));
|
||||
try {
|
||||
await api.post(`/org/${orgId}/domain/${domainId}/restart`);
|
||||
toast({
|
||||
title: t("success"),
|
||||
description: t("domainRestartedDescription", {
|
||||
fallback: "Domain verification restarted successfully",
|
||||
}),
|
||||
});
|
||||
refreshData();
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setRestartingDomains((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(domainId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
const isRestarting = restartingDomains.has(domain.domainId);
|
||||
|
||||
if (!domain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isRestarting = restartingDomains.has(domain.domainId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<SettingsSectionTitle
|
||||
title={domain.baseDomain}
|
||||
description={t("domainSettingDescription")}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => restartDomain(domain.domainId)}
|
||||
disabled={isRestarting}
|
||||
>
|
||||
{isRestarting ? (
|
||||
<>
|
||||
<RefreshCw
|
||||
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("restarting", { fallback: "Restarting..." })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw
|
||||
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("restart", { fallback: "Restart" })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<DomainInfoCard orgId={orgId} domainId={domain.domainId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<SettingsSectionTitle
|
||||
title={domain.baseDomain}
|
||||
description={t("domainSettingDescription")}
|
||||
/>
|
||||
{env.flags.usePangolinDns && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => restartDomain(domain.domainId)}
|
||||
disabled={isRestarting}
|
||||
>
|
||||
{isRestarting ? (
|
||||
<>
|
||||
<RefreshCw
|
||||
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("restarting", { fallback: "Restarting..." })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw
|
||||
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("restart", { fallback: "Restart" })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<DomainInfoCard orgId={orgId} domainId={domain.domainId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ export default function GeneralPage() {
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
<DropdownMenuContent>
|
||||
{LOG_RETENTION_OPTIONS.filter(
|
||||
(option) => {
|
||||
if (
|
||||
@@ -627,243 +627,256 @@ export default function GeneralPage() {
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
|
||||
{build !== "oss" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("securitySettings")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("securitySettingsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SecurityFeaturesAlert />
|
||||
|
||||
{/* Security Settings Section */}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("securitySettings")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("securitySettingsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SecurityFeaturesAlert />
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="security-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requireTwoFactor"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
isSecurityFeatureDisabled();
|
||||
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="security-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requireTwoFactor"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
isSecurityFeatureDisabled();
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="require-two-factor"
|
||||
defaultChecked={
|
||||
field.value ||
|
||||
false
|
||||
}
|
||||
label={t(
|
||||
"requireTwoFactorForAllUsers"
|
||||
)}
|
||||
disabled={
|
||||
isDisabled
|
||||
}
|
||||
onCheckedChange={(
|
||||
val
|
||||
) => {
|
||||
if (
|
||||
!isDisabled
|
||||
) {
|
||||
form.setValue(
|
||||
"requireTwoFactor",
|
||||
val
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"requireTwoFactorDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="maxSessionLengthHours"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
isSecurityFeatureDisabled();
|
||||
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
<div className="flex items-center gap-2">
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>
|
||||
{t("maxSessionLength")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="require-two-factor"
|
||||
defaultChecked={
|
||||
field.value ||
|
||||
false
|
||||
<Select
|
||||
value={
|
||||
field.value?.toString() ||
|
||||
"null"
|
||||
}
|
||||
label={t(
|
||||
"requireTwoFactorForAllUsers"
|
||||
)}
|
||||
disabled={
|
||||
isDisabled
|
||||
}
|
||||
onCheckedChange={(
|
||||
val
|
||||
onValueChange={(
|
||||
value
|
||||
) => {
|
||||
if (
|
||||
!isDisabled
|
||||
) {
|
||||
const numValue =
|
||||
value ===
|
||||
"null"
|
||||
? null
|
||||
: parseInt(
|
||||
value,
|
||||
10
|
||||
);
|
||||
form.setValue(
|
||||
"requireTwoFactor",
|
||||
val
|
||||
"maxSessionLengthHours",
|
||||
numValue
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
disabled={
|
||||
isDisabled
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"selectSessionLength"
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SESSION_LENGTH_OPTIONS.map(
|
||||
(
|
||||
option
|
||||
) => (
|
||||
<SelectItem
|
||||
key={
|
||||
option.value ===
|
||||
null
|
||||
? "null"
|
||||
: option.value.toString()
|
||||
}
|
||||
value={
|
||||
option.value ===
|
||||
null
|
||||
? "null"
|
||||
: option.value.toString()
|
||||
}
|
||||
>
|
||||
{t(
|
||||
option.labelKey
|
||||
)}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"requireTwoFactorDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="maxSessionLengthHours"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
isSecurityFeatureDisabled();
|
||||
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>
|
||||
{t("maxSessionLength")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={
|
||||
field.value?.toString() ||
|
||||
"null"
|
||||
}
|
||||
onValueChange={(
|
||||
value
|
||||
) => {
|
||||
if (!isDisabled) {
|
||||
const numValue =
|
||||
value ===
|
||||
"null"
|
||||
? null
|
||||
: parseInt(
|
||||
value,
|
||||
10
|
||||
);
|
||||
form.setValue(
|
||||
"maxSessionLengthHours",
|
||||
numValue
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"selectSessionLength"
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SESSION_LENGTH_OPTIONS.map(
|
||||
(option) => (
|
||||
<SelectItem
|
||||
key={
|
||||
option.value ===
|
||||
null
|
||||
? "null"
|
||||
: option.value.toString()
|
||||
}
|
||||
value={
|
||||
option.value ===
|
||||
null
|
||||
? "null"
|
||||
: option.value.toString()
|
||||
}
|
||||
>
|
||||
{t(
|
||||
option.labelKey
|
||||
)}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"maxSessionLengthDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="passwordExpiryDays"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
isSecurityFeatureDisabled();
|
||||
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>
|
||||
{t("passwordExpiryDays")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={
|
||||
field.value?.toString() ||
|
||||
"null"
|
||||
}
|
||||
onValueChange={(
|
||||
value
|
||||
) => {
|
||||
if (!isDisabled) {
|
||||
const numValue =
|
||||
value ===
|
||||
"null"
|
||||
? null
|
||||
: parseInt(
|
||||
value,
|
||||
10
|
||||
);
|
||||
form.setValue(
|
||||
"passwordExpiryDays",
|
||||
numValue
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"selectPasswordExpiry"
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PASSWORD_EXPIRY_OPTIONS.map(
|
||||
(option) => (
|
||||
<SelectItem
|
||||
key={
|
||||
option.value ===
|
||||
null
|
||||
? "null"
|
||||
: option.value.toString()
|
||||
}
|
||||
value={
|
||||
option.value ===
|
||||
null
|
||||
? "null"
|
||||
: option.value.toString()
|
||||
}
|
||||
>
|
||||
{t(
|
||||
option.labelKey
|
||||
)}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<FormMessage />
|
||||
{t(
|
||||
"editPasswordExpiryDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"maxSessionLengthDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="passwordExpiryDays"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
isSecurityFeatureDisabled();
|
||||
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>
|
||||
{t(
|
||||
"passwordExpiryDays"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={
|
||||
field.value?.toString() ||
|
||||
"null"
|
||||
}
|
||||
onValueChange={(
|
||||
value
|
||||
) => {
|
||||
if (
|
||||
!isDisabled
|
||||
) {
|
||||
const numValue =
|
||||
value ===
|
||||
"null"
|
||||
? null
|
||||
: parseInt(
|
||||
value,
|
||||
10
|
||||
);
|
||||
form.setValue(
|
||||
"passwordExpiryDays",
|
||||
numValue
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
isDisabled
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"selectPasswordExpiry"
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PASSWORD_EXPIRY_OPTIONS.map(
|
||||
(
|
||||
option
|
||||
) => (
|
||||
<SelectItem
|
||||
key={
|
||||
option.value ===
|
||||
null
|
||||
? "null"
|
||||
: option.value.toString()
|
||||
}
|
||||
value={
|
||||
option.value ===
|
||||
null
|
||||
? "null"
|
||||
: option.value.toString()
|
||||
}
|
||||
>
|
||||
{t(
|
||||
option.labelKey
|
||||
)}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<FormMessage />
|
||||
{t(
|
||||
"editPasswordExpiryDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
|
||||
|
||||
|
||||
@@ -18,9 +18,15 @@ type Props = {
|
||||
records: DNSRecordRow[];
|
||||
domainId: string;
|
||||
isRefreshing?: boolean;
|
||||
type: string | null;
|
||||
};
|
||||
|
||||
export default function DNSRecordsTable({ records, domainId, isRefreshing }: Props) {
|
||||
export default function DNSRecordsTable({
|
||||
records,
|
||||
domainId,
|
||||
isRefreshing,
|
||||
type
|
||||
}: Props) {
|
||||
const t = useTranslations();
|
||||
|
||||
const columns: ColumnDef<DNSRecordRow>[] = [
|
||||
@@ -28,56 +34,31 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro
|
||||
accessorKey: "baseDomain",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div
|
||||
>
|
||||
{t("recordName", { fallback: "Record name" })}
|
||||
</div>
|
||||
<div>{t("recordName", { fallback: "Record name" })}</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const baseDomain = row.original.baseDomain;
|
||||
return (
|
||||
<div>
|
||||
{baseDomain || "-"}
|
||||
</div>
|
||||
);
|
||||
return <div>{baseDomain || "-"}</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "recordType",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div
|
||||
>
|
||||
{t("type")}
|
||||
</div>
|
||||
);
|
||||
return <div>{t("type")}</div>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const type = row.original.recordType;
|
||||
return (
|
||||
<div className="">
|
||||
{type}
|
||||
</div>
|
||||
);
|
||||
return <div className="">{type}</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "ttl",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div
|
||||
>
|
||||
{t("TTL")}
|
||||
</div>
|
||||
);
|
||||
return <div>{t("TTL")}</div>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div>
|
||||
{t("auto")}
|
||||
</div>
|
||||
);
|
||||
return <div>{t("auto")}</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -87,44 +68,39 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const value = row.original.value;
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "verified",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<div
|
||||
>
|
||||
{t("status")}
|
||||
</div>
|
||||
);
|
||||
return <div>{t("status")}</div>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const verified = row.original.verified;
|
||||
return (
|
||||
verified ? (
|
||||
<Badge variant="green">{t("verified")}</Badge>
|
||||
) : (
|
||||
<Badge variant="yellow">
|
||||
{t("pending", { fallback: "Pending" })}
|
||||
return verified ? (
|
||||
type === "wildcard" ? (
|
||||
<Badge variant="outlinePrimary">
|
||||
{t("manual", { fallback: "Manual" })}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="green">{t("verified")}</Badge>
|
||||
)
|
||||
) : (
|
||||
<Badge variant="yellow">
|
||||
{t("pending", { fallback: "Pending" })}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<DNSRecordsDataTable
|
||||
columns={columns}
|
||||
data={records}
|
||||
isRefreshing={isRefreshing}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Badge } from "./ui/badge";
|
||||
|
||||
import Link from "next/link";
|
||||
import { build } from "@server/build";
|
||||
|
||||
type TabFilter = {
|
||||
id: string;
|
||||
@@ -55,6 +56,7 @@ type DNSRecordsDataTableProps<TData, TValue> = {
|
||||
defaultTab?: string;
|
||||
persistPageSize?: boolean | string;
|
||||
defaultPageSize?: number;
|
||||
type?: string | null;
|
||||
};
|
||||
|
||||
export function DNSRecordsDataTable<TData, TValue>({
|
||||
@@ -68,7 +70,7 @@ export function DNSRecordsDataTable<TData, TValue>({
|
||||
defaultSort,
|
||||
tabs,
|
||||
defaultTab,
|
||||
|
||||
type
|
||||
}: DNSRecordsDataTableProps<TData, TValue>) {
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -97,12 +99,9 @@ export function DNSRecordsDataTable<TData, TValue>({
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel()
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-12xl">
|
||||
<Card>
|
||||
@@ -112,28 +111,31 @@ export function DNSRecordsDataTable<TData, TValue>({
|
||||
<h1 className="font-bold">{t("dnsRecord")}</h1>
|
||||
<Badge variant="secondary">{t("required")}</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-1"/>
|
||||
{t("howToAddRecords")}
|
||||
</Button>
|
||||
<Link href="https://docs.pangolin.net/self-host/dns-and-networking">
|
||||
<Button variant="outline">
|
||||
<ExternalLink className="h-4 w-4 mr-1" />
|
||||
{t("howToAddRecords")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-secondary dark:bg-transparent">
|
||||
<TableRow
|
||||
key={headerGroup.id}
|
||||
className="bg-secondary dark:bg-transparent"
|
||||
>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
header.column.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
@@ -10,7 +10,16 @@ import {
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useDomainContext } from "@app/hooks/useDomainContext";
|
||||
import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "./Settings";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionFooter,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "./Settings";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Form,
|
||||
@@ -21,7 +30,13 @@ import {
|
||||
FormMessage,
|
||||
FormDescription
|
||||
} from "@app/components/ui/form";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "./ui/select";
|
||||
import { Input } from "./ui/input";
|
||||
import { useForm } from "react-hook-form";
|
||||
import z from "zod";
|
||||
@@ -51,7 +66,6 @@ function toPunycode(domain: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isValidDomainFormat(domain: string): boolean {
|
||||
const unicodeRegex = /^(?!:\/\/)([^\s.]+\.)*[^\s.]+$/;
|
||||
|
||||
@@ -59,9 +73,9 @@ function isValidDomainFormat(domain: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = domain.split('.');
|
||||
const parts = domain.split(".");
|
||||
for (const part of parts) {
|
||||
if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) {
|
||||
if (part.length === 0 || part.startsWith("-") || part.endsWith("-")) {
|
||||
return false;
|
||||
}
|
||||
if (part.length > 63) {
|
||||
@@ -94,8 +108,10 @@ const certResolverOptions = [
|
||||
{ id: "custom", title: "Custom Resolver" }
|
||||
];
|
||||
|
||||
|
||||
export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) {
|
||||
export default function DomainInfoCard({
|
||||
orgId,
|
||||
domainId
|
||||
}: DomainInfoCardProps) {
|
||||
const { domain, updateDomain } = useDomainContext();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
@@ -111,21 +127,24 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
baseDomain: "",
|
||||
type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
|
||||
certResolver: domain.certResolver ?? "",
|
||||
type:
|
||||
build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
|
||||
certResolver: domain.certResolver,
|
||||
preferWildcardCert: false
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (domain.domainId) {
|
||||
const certResolverValue = domain.certResolver && domain.certResolver.trim() !== ""
|
||||
? domain.certResolver
|
||||
: null;
|
||||
const certResolverValue =
|
||||
domain.certResolver && domain.certResolver.trim() !== ""
|
||||
? domain.certResolver
|
||||
: null;
|
||||
|
||||
form.reset({
|
||||
baseDomain: domain.baseDomain || "",
|
||||
type: (domain.type as "ns" | "cname" | "wildcard") || "wildcard",
|
||||
type:
|
||||
(domain.type as "ns" | "cname" | "wildcard") || "wildcard",
|
||||
certResolver: certResolverValue,
|
||||
preferWildcardCert: domain.preferWildcardCert || false
|
||||
});
|
||||
@@ -170,7 +189,9 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
if (!orgId || !domainId) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("orgOrDomainIdMissing", { fallback: "Organization or Domain ID is missing" }),
|
||||
description: t("orgOrDomainIdMissing", {
|
||||
fallback: "Organization or Domain ID is missing"
|
||||
}),
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
@@ -179,7 +200,11 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
setSaveLoading(true);
|
||||
|
||||
try {
|
||||
const response = await api.patch(
|
||||
if (!values.certResolver) {
|
||||
values.certResolver = null;
|
||||
}
|
||||
|
||||
await api.patch(
|
||||
`/org/${orgId}/domain/${domainId}`,
|
||||
{
|
||||
certResolver: values.certResolver,
|
||||
@@ -195,7 +220,9 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
|
||||
toast({
|
||||
title: t("success"),
|
||||
description: t("domainSettingsUpdated", { fallback: "Domain settings updated successfully" }),
|
||||
description: t("domainSettingsUpdated", {
|
||||
fallback: "Domain settings updated successfully"
|
||||
}),
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -222,30 +249,36 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("type")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionTitle>{t("type")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<span>
|
||||
{getTypeDisplay(domain.type ? domain.type : "")}
|
||||
{getTypeDisplay(
|
||||
domain.type ? domain.type : ""
|
||||
)}
|
||||
</span>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("status")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{domain.verified ? (
|
||||
<Badge variant="green">{t("verified")}</Badge>
|
||||
domain.type === "wildcard" ? (
|
||||
<Badge variant="outlinePrimary">
|
||||
{t("manual", {
|
||||
fallback: "Manual"
|
||||
})}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="green">
|
||||
{t("verified")}
|
||||
</Badge>
|
||||
)
|
||||
) : (
|
||||
<Badge variant="yellow">
|
||||
{t("pending", { fallback: "Pending" })}
|
||||
@@ -257,20 +290,13 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{loadingRecords ? (
|
||||
<div className="space-y-4">
|
||||
{t("loadingDNSRecords", { fallback: "Loading DNS Records..." })}
|
||||
</div>
|
||||
) : (
|
||||
<DNSRecordsTable
|
||||
domainId={domain.domainId}
|
||||
records={dnsRecords}
|
||||
isRefreshing={isRefreshing}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<DNSRecordsTable
|
||||
domainId={domain.domainId}
|
||||
records={dnsRecords}
|
||||
isRefreshing={isRefreshing}
|
||||
type={domain.type}
|
||||
/>
|
||||
|
||||
{/* Domain Settings - Only show for wildcard domains */}
|
||||
{domain.type === "wildcard" && (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
@@ -294,33 +320,73 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
name="certResolver"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("certResolver")}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("certResolver")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={
|
||||
field.value === null ? "default" :
|
||||
(field.value === "" || (field.value && field.value !== "default")) ? "custom" :
|
||||
"default"
|
||||
field.value ===
|
||||
null
|
||||
? "default"
|
||||
: field.value ===
|
||||
"" ||
|
||||
(field.value &&
|
||||
field.value !==
|
||||
"default")
|
||||
? "custom"
|
||||
: "default"
|
||||
}
|
||||
onValueChange={(val) => {
|
||||
if (val === "default") {
|
||||
field.onChange(null);
|
||||
} else if (val === "custom") {
|
||||
field.onChange("");
|
||||
onValueChange={(
|
||||
val
|
||||
) => {
|
||||
if (
|
||||
val ===
|
||||
"default"
|
||||
) {
|
||||
field.onChange(
|
||||
null
|
||||
);
|
||||
} else if (
|
||||
val ===
|
||||
"custom"
|
||||
) {
|
||||
field.onChange(
|
||||
""
|
||||
);
|
||||
} else {
|
||||
field.onChange(val);
|
||||
field.onChange(
|
||||
val
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("selectCertResolver")} />
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"selectCertResolver"
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{certResolverOptions.map((opt) => (
|
||||
<SelectItem key={opt.id} value={opt.id}>
|
||||
{opt.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
{certResolverOptions.map(
|
||||
(
|
||||
opt
|
||||
) => (
|
||||
<SelectItem
|
||||
key={
|
||||
opt.id
|
||||
}
|
||||
value={
|
||||
opt.id
|
||||
}
|
||||
>
|
||||
{
|
||||
opt.title
|
||||
}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
@@ -328,8 +394,10 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{form.watch("certResolver") !== null &&
|
||||
form.watch("certResolver") !== "default" && (
|
||||
{form.watch("certResolver") !==
|
||||
null &&
|
||||
form.watch("certResolver") !==
|
||||
"default" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certResolver"
|
||||
@@ -337,9 +405,22 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t("enterCustomResolver")}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
placeholder={t(
|
||||
"enterCustomResolver"
|
||||
)}
|
||||
value={
|
||||
field.value ||
|
||||
""
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
field.onChange(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -348,25 +429,39 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
/>
|
||||
)}
|
||||
|
||||
{form.watch("certResolver") !== null &&
|
||||
form.watch("certResolver") !== "default" && (
|
||||
{form.watch("certResolver") !==
|
||||
null &&
|
||||
form.watch("certResolver") !==
|
||||
"default" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="preferWildcardCert"
|
||||
render={({ field: switchField }) => (
|
||||
render={({
|
||||
field: switchField
|
||||
}) => (
|
||||
<FormItem className="items-center space-y-2 mt-4">
|
||||
<FormControl>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={switchField.value}
|
||||
onCheckedChange={switchField.onChange}
|
||||
checked={
|
||||
switchField.value
|
||||
}
|
||||
onCheckedChange={
|
||||
switchField.onChange
|
||||
}
|
||||
/>
|
||||
<FormLabel>{t("preferWildcardCert")}</FormLabel>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"preferWildcardCert"
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
{t("preferWildcardCertDescription")}
|
||||
{t(
|
||||
"preferWildcardCertDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -394,4 +489,4 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user