diff --git a/messages/en-US.json b/messages/en-US.json index 07ab4d6e8..e0ffdba1a 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -982,6 +982,8 @@ "resourcePolicySharedDescription": "This resource uses a shared policy.", "sharedPolicy": "Shared 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 {policyName}.", "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 {policyName}.", "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 {policyName}.", "resourceUsersRoles": "Access Controls", @@ -1008,7 +1010,14 @@ "resourceVisibilityTitle": "Visibility", "resourceVisibilityTitleDescription": "Completely enable or disable resource visibility", "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", "resourceTransfer": "Transfer Resource", "resourceTransferDescription": "Transfer this resource to a different site", diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/general/page.tsx index 772c19624..d25ed9363 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/general/page.tsx @@ -11,7 +11,6 @@ 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 DomainPicker from "@app/components/DomainPicker"; import { @@ -24,10 +23,12 @@ import { SettingsFormGrid, SettingsSectionForm, SettingsSectionHeader, - SettingsSectionTitle + SettingsSectionTitle, + SettingsSubsectionDescription, + SettingsSubsectionHeader, + SettingsSubsectionTitle } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; -import { Label } from "@app/components/ui/label"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; @@ -37,7 +38,6 @@ 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 { toASCII, toUnicode } from "punycode"; @@ -47,400 +47,15 @@ import { zodResolver } from "@hookform/resolvers/zod"; import z from "zod"; import { SharedPolicySelect } from "@app/components/shared-policy-selector"; 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 { 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 { tierMatrix } from "@server/lib/billing/tierMatrix"; 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>( - `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 ( - - - - {t("maintenanceMode")} - - - {t("maintenanceModeDescription")} - - - - - - -
- - { - const isDisabled = - !isPaidUser(tierMatrix.maintencePage) || - !["http", "ssh", "rdp", "vnc"].includes( - resource.mode - ); - - return ( - -
- - - - -
- { - if ( - !isDisabled - ) { - maintenanceForm.setValue( - "maintenanceModeEnabled", - val - ); - } - }} - /> -
-
-
-
-
-
- - {t( - "enableMaintenanceModeDescription" - )} - - -
- ); - }} - /> - - {isMaintenanceEnabled && ( -
- ( - - - {t("maintenanceModeType")} - - - - - - - -
- - - {t( - "automatic" - )} - {" "} - ( - {t( - "recommended" - )} - ) - - - {t( - "automaticModeDescription" - )} - -
-
- - - - -
- - - {t( - "forced" - )} - - - - {t( - "forcedModeDescription" - )} - -
-
-
-
- -
- )} - /> - - {maintenanceModeType === "forced" && ( - - - - {t("forcedeModeWarning")} - - - )} - - ( - - - {t("pageTitle")} - - - - - - {t("pageTitleDescription")} - - - - )} - /> - - ( - - - {t( - "maintenancePageMessage" - )} - - -