Merge UI into new screen

This commit is contained in:
Owen
2025-12-20 16:28:41 -05:00
committed by Owen Schwartz
parent 03d1f4bbb9
commit a9a0fbe244
5 changed files with 298 additions and 878 deletions

View File

@@ -1,22 +1,16 @@
import { build } from "@server/build";
import license from "@server/license/license";
import license from "#dynamic/license/license";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "./billing/tiers";
import { TierId } from "@server/lib/billing/tiers";
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
if (build === "enterprise") {
const isUnlocked = await license.isUnlocked();
if (!isUnlocked) {
return false;
}
return await license.isUnlocked();
}
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return false;
}
return tier === TierId.STANDARD;
}
return true;

View File

@@ -430,6 +430,7 @@ export async function getTraefikConfig(
continue;
}
const domainParts = fullDomain.split(".");
let wildCard;
if (domainParts.length <= 2) {

View File

@@ -343,6 +343,7 @@ async function updateHttpResource(
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
if (build == "enterprise" && !isLicensed) {
logger.warn("Server is not licensed! Clearing set maintenance screen values")
// null the maintenance mode fields if not licensed
updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined;

View File

@@ -1,858 +0,0 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { formatAxiosError } from "@app/lib/api";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { ListSitesResponse } from "@server/routers/site";
import { useEffect, useState } from "react";
import { AxiosResponse } from "axios";
import { useParams, useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "@app/hooks/useToast";
import {
SettingsContainer,
SettingsSection,
SettingsSectionHeader,
SettingsSectionTitle,
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionForm,
SettingsSectionFooter
} from "@app/components/Settings";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { Label } from "@app/components/ui/label";
import { ListDomainsResponse } from "@server/routers/domain";
import { UpdateResourceResponse } from "@server/routers/resource";
import { SwitchInput } from "@app/components/SwitchInput";
import { useTranslations } from "next-intl";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import DomainPicker from "@app/components/DomainPicker";
import { AlertCircle, Globe, Info } from "lucide-react";
import { build } from "@server/build";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
import { DomainRow } from "../../../../../../components/DomainsTable";
import { toASCII, toUnicode } from "punycode";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { useUserContext } from "@app/hooks/useUserContext";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@app/components/ui/tooltip";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
export default function GeneralForm() {
const [formKey, setFormKey] = useState(0);
const params = useParams();
const { resource, updateResource } = useResourceContext();
const { org } = useOrgContext();
const router = useRouter();
const t = useTranslations();
const [editDomainOpen, setEditDomainOpen] = useState(false);
const subscriptionStatus = useSubscriptionStatusContext();
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
const subscription = useSubscriptionStatusContext();
const { user } = useUserContext();
const { env } = useEnvContext();
const orgId = params.orgId;
const api = createApiClient({ env });
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
const [saveLoading, setSaveLoading] = useState(false);
const [transferLoading, setTransferLoading] = useState(false);
const [open, setOpen] = useState(false);
const [baseDomains, setBaseDomains] = useState<
ListDomainsResponse["domains"]
>([]);
const [loadingPage, setLoadingPage] = useState(true);
const [resourceFullDomain, setResourceFullDomain] = useState(
`${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`
);
const [selectedDomain, setSelectedDomain] = useState<{
domainId: string;
subdomain?: string;
fullDomain: string;
baseDomain: string;
} | null>(null);
// Check if security features are disabled due to licensing/subscription
const isSecurityFeatureDisabled = () => {
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed();
return isEnterpriseNotLicensed || isSaasNotSubscribed;
};
const GeneralFormSchema = z
.object({
enabled: z.boolean(),
subdomain: z.string().optional(),
name: z.string().min(1).max(255),
niceId: z.string().min(1).max(255).optional(),
domainId: z.string().optional(),
proxyPort: z.number().int().min(1).max(65535).optional(),
// enableProxy: z.boolean().optional()
maintenanceModeEnabled: z.boolean().optional(),
maintenanceModeType: z.enum(["forced", "automatic"]).optional(),
maintenanceTitle: z.string().max(255).optional(),
maintenanceMessage: z.string().max(2000).optional(),
maintenanceEstimatedTime: z.string().max(100).optional()
})
.refine(
(data) => {
// For non-HTTP resources, proxyPort should be defined
if (!resource.http) {
return data.proxyPort !== undefined;
}
// For HTTP resources, proxyPort should be undefined
return data.proxyPort === undefined;
},
{
message: !resource.http
? "Port number is required for non-HTTP resources"
: "Port number should not be set for HTTP resources",
path: ["proxyPort"]
}
);
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
const form = useForm({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
enabled: resource.enabled,
name: resource.name,
niceId: resource.niceId,
subdomain: resource.subdomain ? resource.subdomain : undefined,
domainId: resource.domainId || undefined,
proxyPort: resource.proxyPort || undefined,
// enableProxy: resource.enableProxy || false
maintenanceModeEnabled: resource.maintenanceModeEnabled || false,
maintenanceModeType: resource.maintenanceModeType || "automatic",
maintenanceTitle:
resource.maintenanceTitle || "We'll be back soon!",
maintenanceMessage:
resource.maintenanceMessage ||
"We are currently performing scheduled maintenance. Please check back soon.",
maintenanceEstimatedTime: resource.maintenanceEstimatedTime || ""
},
mode: "onChange"
});
const isMaintenanceEnabled = form.watch("maintenanceModeEnabled");
const maintenanceModeType = form.watch("maintenanceModeType");
useEffect(() => {
const fetchSites = async () => {
const res = await api.get<AxiosResponse<ListSitesResponse>>(
`/org/${orgId}/sites/`
);
setSites(res.data.data.sites);
};
const fetchDomains = async () => {
const res = await api
.get<
AxiosResponse<ListDomainsResponse>
>(`/org/${orgId}/domains/`)
.catch((e) => {
toast({
variant: "destructive",
title: t("domainErrorFetch"),
description: formatAxiosError(
e,
t("domainErrorFetchDescription")
)
});
});
if (res?.status === 200) {
const rawDomains = res.data.data.domains as DomainRow[];
const domains = rawDomains.map((domain) => ({
...domain,
baseDomain: toUnicode(domain.baseDomain)
}));
setBaseDomains(domains);
setFormKey((key) => key + 1);
}
};
const load = async () => {
await fetchDomains();
await fetchSites();
setLoadingPage(false);
};
load();
}, []);
async function onSubmit(data: GeneralFormValues) {
setSaveLoading(true);
const res = await api
.post<AxiosResponse<UpdateResourceResponse>>(
`resource/${resource?.resourceId}`,
{
enabled: data.enabled,
name: data.name,
niceId: data.niceId,
subdomain: data.subdomain
? toASCII(data.subdomain)
: undefined,
domainId: data.domainId,
proxyPort: data.proxyPort,
// ...(!resource.http && {
// enableProxy: data.enableProxy
// })
maintenanceModeEnabled: data.maintenanceModeEnabled,
maintenanceModeType: data.maintenanceModeType,
maintenanceTitle: data.maintenanceTitle || null,
maintenanceMessage: data.maintenanceMessage || null,
maintenanceEstimatedTime:
data.maintenanceEstimatedTime || null
}
)
.catch((e) => {
toast({
variant: "destructive",
title: t("resourceErrorUpdate"),
description: formatAxiosError(
e,
t("resourceErrorUpdateDescription")
)
});
});
if (res && res.status === 200) {
const updated = res.data.data;
updateResource({
enabled: data.enabled,
name: data.name,
niceId: data.niceId,
subdomain: data.subdomain,
fullDomain: resource.fullDomain,
proxyPort: data.proxyPort,
// ...(!resource.http && {
// enableProxy: data.enableProxy
// })
maintenanceModeEnabled: data.maintenanceModeEnabled,
maintenanceModeType: data.maintenanceModeType,
maintenanceTitle: data.maintenanceTitle || null,
maintenanceMessage: data.maintenanceMessage || null,
maintenanceEstimatedTime: data.maintenanceEstimatedTime || null
});
toast({
title: t("resourceUpdated"),
description: t("resourceUpdatedDescription")
});
if (data.niceId && data.niceId !== resource?.niceId) {
router.replace(
`/${updated.orgId}/settings/resources/${data.niceId}/general`
);
} else {
router.refresh();
}
setSaveLoading(false);
}
setSaveLoading(false);
}
return (
!loadingPage && (
<>
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("resourceGeneral")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourceGeneralDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...form} key={formKey}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="general-settings-form"
>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="col-span-2">
<div className="flex items-center space-x-2">
<FormControl>
<SwitchInput
id="enable-resource"
defaultChecked={
resource.enabled
}
label={t(
"resourceEnable"
)}
onCheckedChange={(
val
) =>
form.setValue(
"enabled",
val
)
}
/>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("name")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="niceId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("identifier")}
</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t(
"enterIdentifier"
)}
className="flex-1"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!resource.http && (
<>
<FormField
control={form.control}
name="proxyPort"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"resourcePortNumber"
)}
</FormLabel>
<FormControl>
<Input
type="number"
value={
field.value ??
""
}
onChange={(
e
) =>
field.onChange(
e
.target
.value
? parseInt(
e
.target
.value
)
: undefined
)
}
/>
</FormControl>
<FormMessage />
<FormDescription>
{t(
"resourcePortNumberDescription"
)}
</FormDescription>
</FormItem>
)}
/>
{/* {build == "oss" && (
<FormField
control={form.control}
name="enableProxy"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
variant={
"outlinePrimarySquare"
}
checked={
field.value
}
onCheckedChange={
field.onChange
}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
{t(
"resourceEnableProxy"
)}
</FormLabel>
<FormDescription>
{t(
"resourceEnableProxyDescription"
)}
</FormDescription>
</div>
</FormItem>
)}
/>
)} */}
</>
)}
{resource.http && (
<>
<div className="space-y-2">
<Label>
{t("resourceDomain")}
</Label>
<div className="border p-2 rounded-md flex items-center justify-between">
<span className="text-sm text-muted-foreground flex items-center gap-2">
<Globe size="14" />
{resourceFullDomain}
</span>
<Button
variant="secondary"
type="button"
size="sm"
onClick={() =>
setEditDomainOpen(
true
)
}
>
{t(
"resourceEditDomain"
)}
</Button>
</div>
</div>
</>
)}
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
</SettingsContainer>
{build !== "oss" && resource.http && (
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("maintenanceMode")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("maintenanceModeDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<PaidFeaturesAlert />
<SettingsSectionForm>
<Form {...form}>
<form className="space-y-4">
<FormField
control={form.control}
name="maintenanceModeEnabled"
render={({ field }) => {
const isDisabled =
isSecurityFeatureDisabled();
return (
<FormItem>
<div className="flex items-center space-x-2">
<FormControl>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
asChild
>
<div className="flex items-center gap-2">
<SwitchInput
id="enable-maintenance"
checked={
field.value
}
label={t(
"enableMaintenanceMode"
)}
disabled={
isDisabled
}
onCheckedChange={(
val
) => {
if (
!isDisabled
) {
form.setValue(
"maintenanceModeEnabled",
val
);
}
}}
/>
</div>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
</FormControl>
</div>
<FormDescription>
{t(
"showMaintenancePage"
)}
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
{isMaintenanceEnabled && (
<div className="space-y-4">
<FormField
control={form.control}
name="maintenanceModeType"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>
{t(
"maintenanceModeType"
)}
</FormLabel>
<FormControl>
<RadioGroup
onValueChange={
field.onChange
}
defaultValue={
field.value
}
disabled={isSecurityFeatureDisabled()}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-start space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="automatic" />
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
<strong>
{t(
"automatic"
)}
</strong>{" "}
(
{t(
"recommended"
)}
)
</FormLabel>
<FormDescription>
{t(
"automaticModeDescription"
)}
</FormDescription>
</div>
</FormItem>
<FormItem className="flex items-start space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="forced" />
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
<strong>
{t(
"forced"
)}
</strong>
</FormLabel>
<FormDescription>
{t(
"forcedModeDescription"
)}
</FormDescription>
</div>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{maintenanceModeType ===
"forced" && (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<strong>
{t(
"warning:"
)}
</strong>{" "}
{t(
"forcedeModeWarning"
)}
</AlertDescription>
</Alert>
)}
<FormField
control={form.control}
name="maintenanceTitle"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"pageTitle"
)}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={isSecurityFeatureDisabled()}
placeholder="We'll be back soon!"
/>
</FormControl>
<FormDescription>
{t(
"pageTitleDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="maintenanceMessage"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"maintenancePageMessage"
)}
</FormLabel>
<FormControl>
<Textarea
{...field}
rows={4}
disabled={isSecurityFeatureDisabled()}
placeholder={t(
"maintenancePageMessagePlaceholder"
)}
/>
</FormControl>
<FormDescription>
{t(
"maintenancePageMessageDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="maintenanceEstimatedTime"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"maintenancePageTimeTitle"
)}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={isSecurityFeatureDisabled()}
placeholder={t(
"maintenanceTime"
)}
/>
</FormControl>
<FormDescription>
{t(
"maintenanceEstimatedTimeDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
</SettingsContainer>
)}
<div className="flex justify-end">
<Button
type="submit"
onClick={() => {
console.log(form.getValues());
}}
loading={saveLoading}
disabled={saveLoading}
form="general-settings-form"
>
{t("saveSettings")}
</Button>
</div>
<Credenza
open={editDomainOpen}
onOpenChange={(setOpen) => setEditDomainOpen(setOpen)}
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t("editDomain")}</CredenzaTitle>
<CredenzaDescription>
{t("editDomainDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<DomainPicker
orgId={orgId as string}
cols={1}
onDomainChange={(res) => {
if (!res) {
setSelectedDomain(null);
return;
}
const selected = {
domainId: res.domainId,
subdomain: res.subdomain,
fullDomain: res.fullDomain,
baseDomain: res.baseDomain
};
setSelectedDomain(selected);
}}
/>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t("cancel")}</Button>
</CredenzaClose>
<Button
onClick={() => {
if (selectedDomain) {
const sanitizedSubdomain =
selectedDomain.subdomain
? finalizeSubdomainSanitize(
selectedDomain.subdomain
)
: "";
const sanitizedFullDomain =
sanitizedSubdomain
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
: selectedDomain.baseDomain;
setResourceFullDomain(
`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`
);
form.setValue(
"domainId",
selectedDomain.domainId
);
form.setValue(
"subdomain",
sanitizedSubdomain
);
setEditDomainOpen(false);
}
}}
>
{t("selectDomain")}
</Button>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</>
)
);
}

View File

@@ -11,6 +11,7 @@ import {
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useResourceContext } from "@app/hooks/useResourceContext";
import {
Credenza,
@@ -41,7 +42,7 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
import { UpdateResourceResponse } from "@server/routers/resource";
import { AxiosResponse } from "axios";
import { Globe } from "lucide-react";
import { AlertCircle, Globe } from "lucide-react";
import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation";
import { toASCII, toUnicode } from "punycode";
@@ -49,6 +50,17 @@ import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import { build } from "@server/build";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import {
Tooltip,
TooltipProvider,
TooltipTrigger
} from "@app/components/ui/tooltip";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
export default function GeneralForm() {
const params = useParams();
@@ -56,6 +68,8 @@ export default function GeneralForm() {
const router = useRouter();
const t = useTranslations();
const [editDomainOpen, setEditDomainOpen] = useState(false);
const { isUnlocked } = useLicenseStatusContext();
const subscription = useSubscriptionStatusContext();
const { env } = useEnvContext();
@@ -80,6 +94,14 @@ export default function GeneralForm() {
baseDomain: string;
} | null>(null);
// Check if security features are disabled due to licensing/subscription
const isSecurityFeatureDisabled = () => {
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed();
return isEnterpriseNotLicensed || isSaasNotSubscribed;
};
const GeneralFormSchema = z
.object({
enabled: z.boolean(),
@@ -87,7 +109,12 @@ export default function GeneralForm() {
name: z.string().min(1).max(255),
niceId: z.string().min(1).max(255).optional(),
domainId: z.string().optional(),
proxyPort: z.int().min(1).max(65535).optional()
proxyPort: z.number().int().min(1).max(65535).optional(),
maintenanceModeEnabled: z.boolean().optional(),
maintenanceModeType: z.enum(["forced", "automatic"]).optional(),
maintenanceTitle: z.string().max(255).optional(),
maintenanceMessage: z.string().max(2000).optional(),
maintenanceEstimatedTime: z.string().max(100).optional()
})
.refine(
(data) => {
@@ -106,6 +133,8 @@ export default function GeneralForm() {
}
);
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
const form = useForm({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
@@ -114,11 +143,22 @@ export default function GeneralForm() {
niceId: resource.niceId,
subdomain: resource.subdomain ? resource.subdomain : undefined,
domainId: resource.domainId || undefined,
proxyPort: resource.proxyPort || undefined
proxyPort: resource.proxyPort || undefined,
maintenanceModeEnabled: resource.maintenanceModeEnabled || false,
maintenanceModeType: resource.maintenanceModeType || "automatic",
maintenanceTitle:
resource.maintenanceTitle || "We'll be back soon!",
maintenanceMessage:
resource.maintenanceMessage ||
"We are currently performing scheduled maintenance. Please check back soon.",
maintenanceEstimatedTime: resource.maintenanceEstimatedTime || ""
},
mode: "onChange"
});
const isMaintenanceEnabled = form.watch("maintenanceModeEnabled");
const maintenanceModeType = form.watch("maintenanceModeType");
const [, formAction, saveLoading] = useActionState(onSubmit, null);
async function onSubmit() {
@@ -138,7 +178,13 @@ export default function GeneralForm() {
? toASCII(data.subdomain)
: undefined,
domainId: data.domainId,
proxyPort: data.proxyPort
proxyPort: data.proxyPort,
maintenanceModeEnabled: data.maintenanceModeEnabled,
maintenanceModeType: data.maintenanceModeType,
maintenanceTitle: data.maintenanceTitle || null,
maintenanceMessage: data.maintenanceMessage || null,
maintenanceEstimatedTime:
data.maintenanceEstimatedTime || null
}
)
.catch((e) => {
@@ -162,10 +208,12 @@ export default function GeneralForm() {
subdomain: data.subdomain,
fullDomain: updated.fullDomain,
proxyPort: data.proxyPort,
domainId: data.domainId
// ...(!resource.http && {
// enableProxy: data.enableProxy
// })
domainId: data.domainId,
maintenanceModeEnabled: data.maintenanceModeEnabled,
maintenanceModeType: data.maintenanceModeType,
maintenanceTitle: data.maintenanceTitle || null,
maintenanceMessage: data.maintenanceMessage || null,
maintenanceEstimatedTime: data.maintenanceEstimatedTime || null
});
toast({
@@ -289,8 +337,9 @@ export default function GeneralForm() {
<Input
type="number"
value={
field.value ??
""
field.value !== undefined
? String(field.value)
: ""
}
onChange={(e) =>
field.onChange(
@@ -339,6 +388,239 @@ export default function GeneralForm() {
</div>
</div>
)}
<PaidFeaturesAlert></PaidFeaturesAlert>
<FormField
control={form.control}
name="maintenanceModeEnabled"
render={({ field }) => {
const isDisabled =
isSecurityFeatureDisabled();
return (
<FormItem>
<div className="flex items-center space-x-2">
<FormControl>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
asChild
>
<div className="flex items-center gap-2">
<SwitchInput
id="enable-maintenance"
checked={
field.value
}
label={t(
"enableMaintenanceMode"
)}
disabled={
isDisabled
}
onCheckedChange={(
val
) => {
if (
!isDisabled
) {
form.setValue(
"maintenanceModeEnabled",
val
);
}
}}
/>
</div>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
</FormControl>
</div>
<FormDescription>
{t(
"showMaintenancePage"
)}
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
{isMaintenanceEnabled && (
<div className="space-y-4">
<FormField
control={form.control}
name="maintenanceModeType"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>
{t(
"maintenanceModeType"
)}
</FormLabel>
<FormControl>
<RadioGroup
onValueChange={
field.onChange
}
defaultValue={
field.value
}
disabled={isSecurityFeatureDisabled()}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-start space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="automatic" />
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
<strong>
{t(
"automatic"
)}
</strong>{" "}
(
{t(
"recommended"
)}
)
</FormLabel>
<FormDescription>
{t(
"automaticModeDescription"
)}
</FormDescription>
</div>
</FormItem>
<FormItem className="flex items-start space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="forced" />
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
<strong>
{t(
"forced"
)}
</strong>
</FormLabel>
<FormDescription>
{t(
"forcedModeDescription"
)}
</FormDescription>
</div>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{maintenanceModeType ===
"forced" && (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<strong>
{t("warning:")}
</strong>{" "}
{t(
"forcedeModeWarning"
)}
</AlertDescription>
</Alert>
)}
<FormField
control={form.control}
name="maintenanceTitle"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("pageTitle")}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={isSecurityFeatureDisabled()}
placeholder="We'll be back soon!"
/>
</FormControl>
<FormDescription>
{t(
"pageTitleDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="maintenanceMessage"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"maintenancePageMessage"
)}
</FormLabel>
<FormControl>
<Textarea
{...field}
rows={4}
disabled={isSecurityFeatureDisabled()}
placeholder={t(
"maintenancePageMessagePlaceholder"
)}
/>
</FormControl>
<FormDescription>
{t(
"maintenancePageMessageDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="maintenanceEstimatedTime"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"maintenancePageTimeTitle"
)}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={isSecurityFeatureDisabled()}
placeholder={t(
"maintenanceTime"
)}
/>
</FormControl>
<FormDescription>
{t(
"maintenanceEstimatedTimeDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</form>
</Form>
</SettingsSectionForm>