mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-06 12:19:50 +00:00
move maintenance page config to tab
This commit is contained in:
@@ -982,6 +982,8 @@
|
|||||||
"resourcePolicySharedDescription": "This resource uses a shared policy.",
|
"resourcePolicySharedDescription": "This resource uses a shared policy.",
|
||||||
"sharedPolicy": "Shared Policy",
|
"sharedPolicy": "Shared Policy",
|
||||||
"sharedPolicyNoneDescription": "This resource has its own policy.",
|
"sharedPolicyNoneDescription": "This resource has its own policy.",
|
||||||
|
"resourceSharedPolicyOwnDescription": "This resource has its own authentication and access rules controls.",
|
||||||
|
"resourceSharedPolicyInheritedDescription": "This resource inherits authentication and access rules controls from <policyLink>{policyName}</policyLink>.",
|
||||||
"resourceSharedPolicyAuthenticationNotice": "This resource is using a shared policy. Some authentication settings can be edited on this resource. To change the underlying policy, you must edit to <policyLink>{policyName}</policyLink>.",
|
"resourceSharedPolicyAuthenticationNotice": "This resource is using a shared policy. Some authentication settings can be edited on this resource. To change the underlying policy, you must edit to <policyLink>{policyName}</policyLink>.",
|
||||||
"resourceSharedPolicyRulesNotice": "This resource is using a shared policy. Some access rules can be edited on this resource. To change the underlying policy, you must edit <policyLink>{policyName}</policyLink>.",
|
"resourceSharedPolicyRulesNotice": "This resource is using a shared policy. Some access rules can be edited on this resource. To change the underlying policy, you must edit <policyLink>{policyName}</policyLink>.",
|
||||||
"resourceUsersRoles": "Access Controls",
|
"resourceUsersRoles": "Access Controls",
|
||||||
@@ -1008,7 +1010,14 @@
|
|||||||
"resourceVisibilityTitle": "Visibility",
|
"resourceVisibilityTitle": "Visibility",
|
||||||
"resourceVisibilityTitleDescription": "Completely enable or disable resource visibility",
|
"resourceVisibilityTitleDescription": "Completely enable or disable resource visibility",
|
||||||
"resourceGeneral": "General Settings",
|
"resourceGeneral": "General Settings",
|
||||||
"resourceGeneralDescription": "Configure the general settings for this resource",
|
"resourceGeneralDescription": "Configure name, address, and access policy for this resource.",
|
||||||
|
"resourceGeneralDetailsSubsection": "Resource Details",
|
||||||
|
"resourceGeneralDetailsSubsectionDescription": "Set the display name, identifier, and publicly accessible domain for this resource.",
|
||||||
|
"resourceGeneralDetailsSubsectionPortDescription": "Set the display name, identifier, and public port for this resource.",
|
||||||
|
"resourceGeneralPublicAddressSubsection": "Public Address",
|
||||||
|
"resourceGeneralPublicAddressSubsectionDescription": "Configure how users reach this resource.",
|
||||||
|
"resourceGeneralAuthenticationAccessSubsection": "Authentication & Access",
|
||||||
|
"resourceGeneralAuthenticationAccessSubsectionDescription": "Choose whether this resource uses its own policy or inherits from a shared policy.",
|
||||||
"resourceEnable": "Enable Resource",
|
"resourceEnable": "Enable Resource",
|
||||||
"resourceTransfer": "Transfer Resource",
|
"resourceTransfer": "Transfer Resource",
|
||||||
"resourceTransferDescription": "Transfer this resource to a different site",
|
"resourceTransferDescription": "Transfer this resource to a different site",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
FormMessage
|
FormMessage
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||||
import DomainPicker from "@app/components/DomainPicker";
|
import DomainPicker from "@app/components/DomainPicker";
|
||||||
import {
|
import {
|
||||||
@@ -24,10 +23,12 @@ import {
|
|||||||
SettingsFormGrid,
|
SettingsFormGrid,
|
||||||
SettingsSectionForm,
|
SettingsSectionForm,
|
||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle,
|
||||||
|
SettingsSubsectionDescription,
|
||||||
|
SettingsSubsectionHeader,
|
||||||
|
SettingsSubsectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { Label } from "@app/components/ui/label";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
@@ -37,7 +38,6 @@ import {
|
|||||||
UpdateResourceResponse
|
UpdateResourceResponse
|
||||||
} from "@server/routers/resource";
|
} from "@server/routers/resource";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { AlertCircle } from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { toASCII, toUnicode } from "punycode";
|
import { toASCII, toUnicode } from "punycode";
|
||||||
@@ -47,400 +47,15 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { SharedPolicySelect } from "@app/components/shared-policy-selector";
|
import { SharedPolicySelect } from "@app/components/shared-policy-selector";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
|
import { orgQueries } from "@app/lib/queries";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import Link from "next/link";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { TierFeature } from "@server/lib/billing/tierMatrix";
|
import { TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
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";
|
|
||||||
import { GetResourceResponse } from "@server/routers/resource/getResource";
|
|
||||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import UptimeAlertSection from "@app/components/UptimeAlertSection";
|
import UptimeAlertSection from "@app/components/UptimeAlertSection";
|
||||||
|
|
||||||
type MaintenanceSectionFormProps = {
|
|
||||||
resource: GetResourceResponse;
|
|
||||||
updateResource: ResourceContextType["updateResource"];
|
|
||||||
};
|
|
||||||
|
|
||||||
function MaintenanceSectionForm({
|
|
||||||
resource,
|
|
||||||
updateResource
|
|
||||||
}: MaintenanceSectionFormProps) {
|
|
||||||
const { env } = useEnvContext();
|
|
||||||
const t = useTranslations();
|
|
||||||
const api = createApiClient({ env });
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
|
||||||
|
|
||||||
const MaintenanceFormSchema = z.object({
|
|
||||||
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()
|
|
||||||
});
|
|
||||||
|
|
||||||
const maintenanceForm = useForm({
|
|
||||||
resolver: zodResolver(MaintenanceFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
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 = maintenanceForm.watch(
|
|
||||||
"maintenanceModeEnabled"
|
|
||||||
);
|
|
||||||
const maintenanceModeType = maintenanceForm.watch("maintenanceModeType");
|
|
||||||
|
|
||||||
const [, maintenanceFormAction, maintenanceSaveLoading] = useActionState(
|
|
||||||
onMaintenanceSubmit,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
async function onMaintenanceSubmit() {
|
|
||||||
const isValid = await maintenanceForm.trigger();
|
|
||||||
if (!isValid) return;
|
|
||||||
|
|
||||||
const data = maintenanceForm.getValues();
|
|
||||||
|
|
||||||
const res = await api
|
|
||||||
.post<AxiosResponse<UpdateResourceResponse>>(
|
|
||||||
`resource/${resource?.resourceId}`,
|
|
||||||
{
|
|
||||||
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) {
|
|
||||||
updateResource({
|
|
||||||
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 (!["http", "ssh", "rdp", "vnc"].includes(resource.mode)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
{t("maintenanceMode")}
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
{t("maintenanceModeDescription")}
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.maintencePage} />
|
|
||||||
<SettingsSectionForm>
|
|
||||||
<Form {...maintenanceForm}>
|
|
||||||
<form
|
|
||||||
action={maintenanceFormAction}
|
|
||||||
className="space-y-4"
|
|
||||||
id="maintenance-settings-form"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={maintenanceForm.control}
|
|
||||||
name="maintenanceModeEnabled"
|
|
||||||
render={({ field }) => {
|
|
||||||
const isDisabled =
|
|
||||||
!isPaidUser(tierMatrix.maintencePage) ||
|
|
||||||
!["http", "ssh", "rdp", "vnc"].includes(
|
|
||||||
resource.mode
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
|
||||||
) {
|
|
||||||
maintenanceForm.setValue(
|
|
||||||
"maintenanceModeEnabled",
|
|
||||||
val
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"enableMaintenanceModeDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isMaintenanceEnabled && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={maintenanceForm.control}
|
|
||||||
name="maintenanceModeType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="space-y-3">
|
|
||||||
<FormLabel>
|
|
||||||
{t("maintenanceModeType")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
defaultValue={
|
|
||||||
field.value
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
!isPaidUser(
|
|
||||||
tierMatrix.maintencePage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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 variant={"neutral"}>
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertDescription>
|
|
||||||
{t("forcedeModeWarning")}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={maintenanceForm.control}
|
|
||||||
name="maintenanceTitle"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("pageTitle")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
disabled={
|
|
||||||
!isPaidUser(
|
|
||||||
tierMatrix.maintencePage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder="We'll be back soon!"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t("pageTitleDescription")}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={maintenanceForm.control}
|
|
||||||
name="maintenanceMessage"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"maintenancePageMessage"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
{...field}
|
|
||||||
rows={4}
|
|
||||||
disabled={
|
|
||||||
!isPaidUser(
|
|
||||||
tierMatrix.maintencePage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder={t(
|
|
||||||
"maintenancePageMessagePlaceholder"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"maintenancePageMessageDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={maintenanceForm.control}
|
|
||||||
name="maintenanceEstimatedTime"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"maintenancePageTimeTitle"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
disabled={
|
|
||||||
!isPaidUser(
|
|
||||||
tierMatrix.maintencePage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder={t(
|
|
||||||
"maintenanceTime"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"maintenanceEstimatedTimeDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</SettingsSectionForm>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
|
|
||||||
<SettingsSectionFooter>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
loading={maintenanceSaveLoading}
|
|
||||||
disabled={
|
|
||||||
maintenanceSaveLoading ||
|
|
||||||
!isPaidUser(tierMatrix.maintencePage)
|
|
||||||
}
|
|
||||||
form="maintenance-settings-form"
|
|
||||||
>
|
|
||||||
{t("saveSettings")}
|
|
||||||
</Button>
|
|
||||||
</SettingsSectionFooter>
|
|
||||||
</SettingsSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GeneralForm() {
|
export default function GeneralForm() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
@@ -467,6 +82,13 @@ export default function GeneralForm() {
|
|||||||
setSelectedSharedPolicyId(resource.resourcePolicyId ?? null);
|
setSelectedSharedPolicyId(resource.resourcePolicyId ?? null);
|
||||||
}, [resource.resourcePolicyId]);
|
}, [resource.resourcePolicyId]);
|
||||||
|
|
||||||
|
const { data: selectedSharedPolicy } = useQuery({
|
||||||
|
...orgQueries.resourcePolicy({
|
||||||
|
resourcePolicyId: selectedSharedPolicyId!
|
||||||
|
}),
|
||||||
|
enabled: showResourcePolicy && selectedSharedPolicyId !== null
|
||||||
|
});
|
||||||
|
|
||||||
const [resourceFullDomain, setResourceFullDomain] = useState(
|
const [resourceFullDomain, setResourceFullDomain] = useState(
|
||||||
`${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`
|
`${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`
|
||||||
);
|
);
|
||||||
@@ -672,6 +294,28 @@ export default function GeneralForm() {
|
|||||||
/>
|
/>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
|
|
||||||
|
<SettingsFormCell span="full">
|
||||||
|
<SettingsSubsectionHeader>
|
||||||
|
<SettingsSubsectionTitle>
|
||||||
|
{t(
|
||||||
|
"resourceGeneralDetailsSubsection"
|
||||||
|
)}
|
||||||
|
</SettingsSubsectionTitle>
|
||||||
|
<SettingsSubsectionDescription>
|
||||||
|
{t(
|
||||||
|
[
|
||||||
|
"tcp",
|
||||||
|
"udp",
|
||||||
|
].includes(
|
||||||
|
resource.mode
|
||||||
|
)
|
||||||
|
? "resourceGeneralDetailsSubsectionPortDescription"
|
||||||
|
: "resourceGeneralDetailsSubsectionDescription"
|
||||||
|
)}
|
||||||
|
</SettingsSubsectionDescription>
|
||||||
|
</SettingsSubsectionHeader>
|
||||||
|
</SettingsFormCell>
|
||||||
|
|
||||||
<SettingsFormCell span="half">
|
<SettingsFormCell span="half">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -830,12 +474,27 @@ export default function GeneralForm() {
|
|||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
)}
|
)}
|
||||||
{showResourcePolicy && (
|
{showResourcePolicy && (
|
||||||
<SettingsFormCell span="half">
|
<>
|
||||||
<div className="space-y-2">
|
<SettingsFormCell span="full">
|
||||||
<FormLabel>
|
<SettingsSubsectionHeader>
|
||||||
{t("sharedPolicy")}
|
<SettingsSubsectionTitle>
|
||||||
</FormLabel>
|
{t(
|
||||||
<SharedPolicySelect
|
"resourceGeneralAuthenticationAccessSubsection"
|
||||||
|
)}
|
||||||
|
</SettingsSubsectionTitle>
|
||||||
|
<SettingsSubsectionDescription>
|
||||||
|
{t(
|
||||||
|
"resourceGeneralAuthenticationAccessSubsectionDescription"
|
||||||
|
)}
|
||||||
|
</SettingsSubsectionDescription>
|
||||||
|
</SettingsSubsectionHeader>
|
||||||
|
</SettingsFormCell>
|
||||||
|
<SettingsFormCell span="full">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("sharedPolicy")}
|
||||||
|
</FormLabel>
|
||||||
|
<SharedPolicySelect
|
||||||
key={
|
key={
|
||||||
resource.resourcePolicyId ??
|
resource.resourcePolicyId ??
|
||||||
"none"
|
"none"
|
||||||
@@ -847,9 +506,39 @@ export default function GeneralForm() {
|
|||||||
onChange={
|
onChange={
|
||||||
setSelectedSharedPolicyId
|
setSelectedSharedPolicyId
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
<FormDescription>
|
||||||
</SettingsFormCell>
|
{selectedSharedPolicyId ===
|
||||||
|
null
|
||||||
|
? t(
|
||||||
|
"resourceSharedPolicyOwnDescription"
|
||||||
|
)
|
||||||
|
: selectedSharedPolicy
|
||||||
|
? t.rich(
|
||||||
|
"resourceSharedPolicyInheritedDescription",
|
||||||
|
{
|
||||||
|
policyName:
|
||||||
|
selectedSharedPolicy.name,
|
||||||
|
policyLink:
|
||||||
|
(
|
||||||
|
chunks
|
||||||
|
) => (
|
||||||
|
<Link
|
||||||
|
href={`/${org.org.orgId}/settings/policies/resources/public/${selectedSharedPolicy.niceId}/general`}
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
chunks
|
||||||
|
}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</SettingsFormCell>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</SettingsFormGrid>
|
</SettingsFormGrid>
|
||||||
</form>
|
</form>
|
||||||
@@ -868,13 +557,6 @@ export default function GeneralForm() {
|
|||||||
</Button>
|
</Button>
|
||||||
</SettingsSectionFooter>
|
</SettingsSectionFooter>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
{!env.flags.disableEnterpriseFeatures && (
|
|
||||||
<MaintenanceSectionForm
|
|
||||||
resource={resource}
|
|
||||||
updateResource={updateResource}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import OrgProvider from "@app/providers/OrgProvider";
|
|||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import ResourceInfoBox from "@app/components/ResourceInfoBox";
|
import ResourceInfoBox from "@app/components/ResourceInfoBox";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -30,6 +31,7 @@ interface ResourceLayoutProps {
|
|||||||
export default async function ResourceLayout(props: ResourceLayoutProps) {
|
export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
const env = pullEnv();
|
||||||
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
@@ -102,6 +104,13 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
|||||||
href: `/{orgId}/settings/resources/public/{niceId}/rules`
|
href: `/{orgId}/settings/resources/public/{niceId}/rules`
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!env.flags.disableEnterpriseFeatures) {
|
||||||
|
navItems.push({
|
||||||
|
title: t("maintenanceMode"),
|
||||||
|
href: `/{orgId}/settings/resources/public/{niceId}/maintenance`
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,445 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
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 {
|
||||||
|
SettingsContainer,
|
||||||
|
SettingsSection,
|
||||||
|
SettingsSectionBody,
|
||||||
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionFooter,
|
||||||
|
SettingsSectionForm,
|
||||||
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle
|
||||||
|
} from "@app/components/Settings";
|
||||||
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { UpdateResourceResponse } from "@server/routers/resource";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { AlertCircle } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useActionState, useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import z from "zod";
|
||||||
|
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";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
|
||||||
|
const maintenanceSupportedModes = ["http", "ssh", "rdp", "vnc"];
|
||||||
|
|
||||||
|
export default function ResourceMaintenancePage() {
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
const { resource, updateResource } = useResourceContext();
|
||||||
|
const t = useTranslations();
|
||||||
|
const api = createApiClient({ env });
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
|
||||||
|
const supportsMaintenance = maintenanceSupportedModes.includes(
|
||||||
|
resource.mode
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (env.flags.disableEnterpriseFeatures || !supportsMaintenance) {
|
||||||
|
router.replace(
|
||||||
|
`/${params.orgId}/settings/resources/public/${resource.niceId}/general`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
env.flags.disableEnterpriseFeatures,
|
||||||
|
params.orgId,
|
||||||
|
resource.niceId,
|
||||||
|
router,
|
||||||
|
supportsMaintenance
|
||||||
|
]);
|
||||||
|
|
||||||
|
const MaintenanceFormSchema = z.object({
|
||||||
|
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()
|
||||||
|
});
|
||||||
|
|
||||||
|
const maintenanceForm = useForm({
|
||||||
|
resolver: zodResolver(MaintenanceFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
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 = maintenanceForm.watch(
|
||||||
|
"maintenanceModeEnabled"
|
||||||
|
);
|
||||||
|
const maintenanceModeType = maintenanceForm.watch("maintenanceModeType");
|
||||||
|
|
||||||
|
const [, maintenanceFormAction, maintenanceSaveLoading] = useActionState(
|
||||||
|
onMaintenanceSubmit,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
async function onMaintenanceSubmit() {
|
||||||
|
const isValid = await maintenanceForm.trigger();
|
||||||
|
if (!isValid) return;
|
||||||
|
|
||||||
|
const data = maintenanceForm.getValues();
|
||||||
|
|
||||||
|
const res = await api
|
||||||
|
.post<AxiosResponse<UpdateResourceResponse>>(
|
||||||
|
`resource/${resource?.resourceId}`,
|
||||||
|
{
|
||||||
|
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) {
|
||||||
|
updateResource({
|
||||||
|
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 (env.flags.disableEnterpriseFeatures || !supportsMaintenance) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContainer>
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("maintenanceMode")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("maintenanceModeDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<PaidFeaturesAlert tiers={tierMatrix.maintencePage} />
|
||||||
|
<SettingsSectionForm>
|
||||||
|
<Form {...maintenanceForm}>
|
||||||
|
<form
|
||||||
|
action={maintenanceFormAction}
|
||||||
|
className="space-y-4"
|
||||||
|
id="maintenance-settings-form"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={maintenanceForm.control}
|
||||||
|
name="maintenanceModeEnabled"
|
||||||
|
render={({ field }) => {
|
||||||
|
const isDisabled = !isPaidUser(
|
||||||
|
tierMatrix.maintencePage
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
maintenanceForm.setValue(
|
||||||
|
"maintenanceModeEnabled",
|
||||||
|
val
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"enableMaintenanceModeDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isMaintenanceEnabled && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={maintenanceForm.control}
|
||||||
|
name="maintenanceModeType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="space-y-3">
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"maintenanceModeType"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
defaultValue={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
!isPaidUser(
|
||||||
|
tierMatrix.maintencePage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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 variant={"neutral"}>
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
{t("forcedeModeWarning")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={maintenanceForm.control}
|
||||||
|
name="maintenanceTitle"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("pageTitle")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
disabled={
|
||||||
|
!isPaidUser(
|
||||||
|
tierMatrix.maintencePage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="We'll be back soon!"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"pageTitleDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={maintenanceForm.control}
|
||||||
|
name="maintenanceMessage"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"maintenancePageMessage"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
rows={4}
|
||||||
|
disabled={
|
||||||
|
!isPaidUser(
|
||||||
|
tierMatrix.maintencePage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder={t(
|
||||||
|
"maintenancePageMessagePlaceholder"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"maintenancePageMessageDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={maintenanceForm.control}
|
||||||
|
name="maintenanceEstimatedTime"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"maintenancePageTimeTitle"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
disabled={
|
||||||
|
!isPaidUser(
|
||||||
|
tierMatrix.maintencePage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder={t(
|
||||||
|
"maintenanceTime"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"maintenanceEstimatedTimeDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
|
||||||
|
<SettingsSectionFooter>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={maintenanceSaveLoading}
|
||||||
|
disabled={
|
||||||
|
maintenanceSaveLoading ||
|
||||||
|
!isPaidUser(tierMatrix.maintencePage)
|
||||||
|
}
|
||||||
|
form="maintenance-settings-form"
|
||||||
|
>
|
||||||
|
{t("saveSettings")}
|
||||||
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
|
</SettingsSection>
|
||||||
|
</SettingsContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user