From fb6f5b39531d9c0c8033d9b531adc44de08a2e60 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Tue, 9 Jun 2026 15:40:38 -0700 Subject: [PATCH] form layout improvements --- messages/en-US.json | 6 +- .../settings/clients/machine/create/page.tsx | 8 +- .../settings/general/security/page.tsx | 155 ++++-- .../public/[niceId]/general/page.tsx | 2 +- .../resources/public/[niceId]/http/page.tsx | 456 ++++++++-------- .../public/[niceId]/maintenance/page.tsx | 486 +++++++++--------- .../resources/public/[niceId]/ssh/page.tsx | 3 + .../settings/sites/[niceId]/general/page.tsx | 80 +-- .../[orgId]/settings/sites/create/page.tsx | 25 +- .../resource-policy/CreatePolicyForm.tsx | 39 +- .../EditPolicyNameSectionForm.tsx | 89 ++-- .../resource-policy/PolicyAuthSsoSection.tsx | 191 ++++--- .../PolicyAuthStackSectionCreate.tsx | 155 +++--- .../PolicyAuthStackSectionEdit.tsx | 243 +++++---- 14 files changed, 1041 insertions(+), 897 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index e0ffdba1a..2077ff56b 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -727,7 +727,7 @@ "targetSubmit": "Add Target", "targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to the backend.", "targetNoOneDescription": "Adding more than one target above will enable load balancing.", - "targetsSubmit": "Save Targets", + "targetsSubmit": "Save Settings", "addTarget": "Add Target", "proxyMultiSiteRoundRobinNodeHelp": "Round robin routing will not work between sites that are not connected to the same node, but failover will work.", "targetErrorInvalidIp": "Invalid IP address", @@ -1845,7 +1845,7 @@ "documentation": "Documentation", "saveAllSettings": "Save All Settings", "saveResourceTargets": "Save Targets", - "saveResourceHttp": "Save Proxy Settings", + "saveResourceHttp": "Save Settings", "saveProxyProtocol": "Save Proxy protocol settings", "settingsUpdated": "Settings updated", "settingsUpdatedDescription": "Settings updated successfully", @@ -3180,6 +3180,8 @@ "warning:": "Warning:", "forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.", "pageTitle": "Page Title", + "maintenancePageContentSubsection": "Page Content", + "maintenancePageContentSubsectionDescription": "Customize the content displayed on the maintenance page", "pageTitleDescription": "The main heading displayed on the maintenance page", "maintenancePageMessage": "Maintenance Message", "maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.", diff --git a/src/app/[orgId]/settings/clients/machine/create/page.tsx b/src/app/[orgId]/settings/clients/machine/create/page.tsx index 4ad6a1673..9e680e21f 100644 --- a/src/app/[orgId]/settings/clients/machine/create/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/create/page.tsx @@ -262,7 +262,7 @@ export default function Page() { id="create-client-form" > - + - + {showAdvancedSettings && ( - + {LOG_RETENTION_OPTIONS.filter( (option) => { - if (build != "saas") { + if ( + build != "saas" + ) { return true; } let maxDays: number; - if (!subscriptionTier) { + if ( + !subscriptionTier + ) { // No tier maxDays = 3; - } else if (subscriptionTier == "enterprise") { + } else if ( + subscriptionTier == + "enterprise" + ) { // Enterprise - no limit return true; - } else if (subscriptionTier == "tier3") { + } else if ( + subscriptionTier == + "tier3" + ) { maxDays = 90; - } else if (subscriptionTier == "tier2") { + } else if ( + subscriptionTier == + "tier2" + ) { maxDays = 30; - } else if (subscriptionTier == "tier1") { + } else if ( + subscriptionTier == + "tier1" + ) { maxDays = 7; } else { // Default to most restrictive @@ -249,7 +265,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { // Filter out options that exceed the max // Special values: -1 (forever) and 9001 (end of year) should be filtered - if (option.value < 0 || option.value > maxDays) { + if ( + option.value < + 0 || + option.value > + maxDays + ) { return false; } @@ -322,24 +343,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { {LOG_RETENTION_OPTIONS.filter( - (option) => { - if (build != "saas") { + ( + option + ) => { + if ( + build != + "saas" + ) { return true; } let maxDays: number; - if (!subscriptionTier) { + if ( + !subscriptionTier + ) { // No tier maxDays = 3; - } else if (subscriptionTier == "enterprise") { + } else if ( + subscriptionTier == + "enterprise" + ) { // Enterprise - no limit return true; - } else if (subscriptionTier == "tier3") { + } else if ( + subscriptionTier == + "tier3" + ) { maxDays = 90; - } else if (subscriptionTier == "tier2") { + } else if ( + subscriptionTier == + "tier2" + ) { maxDays = 30; - } else if (subscriptionTier == "tier1") { + } else if ( + subscriptionTier == + "tier1" + ) { maxDays = 7; } else { // Default to most restrictive @@ -348,7 +388,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { // Filter out options that exceed the max // Special values: -1 (forever) and 9001 (end of year) should be filtered - if (option.value < 0 || option.value > maxDays) { + if ( + option.value < + 0 || + option.value > + maxDays + ) { return false; } @@ -423,24 +468,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { {LOG_RETENTION_OPTIONS.filter( - (option) => { - if (build != "saas") { + ( + option + ) => { + if ( + build != + "saas" + ) { return true; } let maxDays: number; - if (!subscriptionTier) { + if ( + !subscriptionTier + ) { // No tier maxDays = 3; - } else if (subscriptionTier == "enterprise") { + } else if ( + subscriptionTier == + "enterprise" + ) { // Enterprise - no limit return true; - } else if (subscriptionTier == "tier3") { + } else if ( + subscriptionTier == + "tier3" + ) { maxDays = 90; - } else if (subscriptionTier == "tier2") { + } else if ( + subscriptionTier == + "tier2" + ) { maxDays = 30; - } else if (subscriptionTier == "tier1") { + } else if ( + subscriptionTier == + "tier1" + ) { maxDays = 7; } else { // Default to most restrictive @@ -449,7 +513,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { // Filter out options that exceed the max // Special values: -1 (forever) and 9001 (end of year) should be filtered - if (option.value < 0 || option.value > maxDays) { + if ( + option.value < + 0 || + option.value > + maxDays + ) { return false; } @@ -524,24 +593,43 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { {LOG_RETENTION_OPTIONS.filter( - (option) => { - if (build != "saas") { + ( + option + ) => { + if ( + build != + "saas" + ) { return true; } let maxDays: number; - if (!subscriptionTier) { + if ( + !subscriptionTier + ) { // No tier maxDays = 3; - } else if (subscriptionTier == "enterprise") { + } else if ( + subscriptionTier == + "enterprise" + ) { // Enterprise - no limit return true; - } else if (subscriptionTier == "tier3") { + } else if ( + subscriptionTier == + "tier3" + ) { maxDays = 90; - } else if (subscriptionTier == "tier2") { + } else if ( + subscriptionTier == + "tier2" + ) { maxDays = 30; - } else if (subscriptionTier == "tier1") { + } else if ( + subscriptionTier == + "tier1" + ) { maxDays = 7; } else { // Default to most restrictive @@ -550,7 +638,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { // Filter out options that exceed the max // Special values: -1 (forever) and 9001 (end of year) should be filtered - if (option.value < 0 || option.value > maxDays) { + if ( + option.value < + 0 || + option.value > + maxDays + ) { return false; } 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 d25ed9363..78775f81e 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/general/page.tsx @@ -489,7 +489,7 @@ export default function GeneralForm() { - +
{t("sharedPolicy")} diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx index ba6e24699..d9e2fefe0 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx @@ -1,35 +1,21 @@ "use client"; -import HealthCheckCredenza from "@/components/HealthCheckCredenza"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; import { HeadersInput } from "@app/components/HeadersInput"; -import { - PathMatchDisplay, - PathMatchModal, - PathRewriteDisplay, - PathRewriteModal -} from "@app/components/PathMatchRenameModal"; -import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item"; import { SettingsContainer, + SettingsFormCell, + SettingsFormGrid, SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; -import { Alert, AlertDescription } from "@app/components/ui/alert"; import { Form, FormControl, @@ -43,35 +29,24 @@ import type { ResourceContextType } from "@app/contexts/resourceContext"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { toast } from "@app/hooks/useToast"; -import { createApiClient } from "@app/lib/api"; -import { formatAxiosError } from "@app/lib/api/formatAxiosError"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { resourceQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; +import { UpdateResourceResponse } from "@server/routers/resource"; import { tlsNameSchema } from "@server/lib/schemas"; import { useQuery } from "@tanstack/react-query"; import { ProxyResourceTargetsForm } from "@app/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm"; -import { - AlertTriangle, -} from "lucide-react"; +import { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; -import { - use, - useActionState, -} from "react"; +import { useParams, useRouter } from "next/navigation"; +import { useActionState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -const targetsSettingsSchema = z.object({ - stickySession: z.boolean() -}); - -export default function ReverseProxyTargetsPage(props: { - params: Promise<{ resourceId: number; orgId: string }>; -}) { - const params = use(props.params); +export default function ReverseProxyTargetsPage() { + const params = useParams(); const { resource, updateResource } = useResourceContext(); const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery( @@ -87,7 +62,7 @@ export default function ReverseProxyTargetsPage(props: { return ( )} - ); } @@ -110,8 +84,12 @@ function ProxyResourceHttpForm({ updateResource }: Pick) { const t = useTranslations(); + const router = useRouter(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); - const tlsSettingsSchema = z.object({ + const httpSettingsSchema = z.object({ + stickySession: z.boolean(), ssl: z.boolean(), tlsServerName: z .string() @@ -126,18 +104,7 @@ function ProxyResourceHttpForm({ { message: t("proxyErrorTls") } - ) - }); - - const tlsSettingsForm = useForm({ - resolver: zodResolver(tlsSettingsSchema), - defaultValues: { - ssl: resource.ssl, - tlsServerName: resource.tlsServerName || "" - } - }); - - const proxySettingsSchema = z.object({ + ), setHostHeader: z .string() .optional() @@ -154,69 +121,59 @@ function ProxyResourceHttpForm({ ), headers: z .array(z.object({ name: z.string(), value: z.string() })) - .nullable(), - proxyProtocol: z.boolean().optional(), - proxyProtocolVersion: z.int().min(1).max(2).optional() + .nullable() }); - const proxySettingsForm = useForm({ - resolver: zodResolver(proxySettingsSchema), + const form = useForm({ + resolver: zodResolver(httpSettingsSchema), defaultValues: { + stickySession: resource.stickySession, + ssl: resource.ssl, + tlsServerName: resource.tlsServerName || "", setHostHeader: resource.setHostHeader || "", - headers: resource.headers, - proxyProtocol: resource.proxyProtocol || false, - proxyProtocolVersion: resource.proxyProtocolVersion || 1 - } + headers: resource.headers + }, + mode: "onChange" }); - const { env } = useEnvContext(); - const api = createApiClient({ env }); + const [, formAction, saveLoading] = useActionState(onSubmit, null); - const targetsSettingsForm = useForm({ - resolver: zodResolver(targetsSettingsSchema), - defaultValues: { - stickySession: resource.stickySession - } - }); + async function onSubmit() { + const isValid = await form.trigger(); + if (!isValid) return; - const router = useRouter(); - const [, formAction, isSubmitting] = useActionState( - saveResourceHttpSettings, - null - ); + const data = form.getValues(); - async function saveResourceHttpSettings() { - const isValidTLS = await tlsSettingsForm.trigger(); - const isValidProxy = await proxySettingsForm.trigger(); - const targetSettingsForm = await targetsSettingsForm.trigger(); - if (!isValidTLS || !isValidProxy || !targetSettingsForm) return; + const res = await api + .post>( + `/resource/${resource.resourceId}`, + { + stickySession: data.stickySession, + ssl: data.ssl, + tlsServerName: data.tlsServerName || null, + setHostHeader: data.setHostHeader || null, + headers: data.headers || null + } + ) + .catch((err) => { + toast({ + variant: "destructive", + title: t("settingsErrorUpdate"), + description: formatAxiosError( + err, + t("settingsErrorUpdateDescription") + ) + }); + }); - try { - // Gather all settings - const stickySessionData = targetsSettingsForm.getValues(); - const tlsData = tlsSettingsForm.getValues(); - const proxyData = proxySettingsForm.getValues(); - - // Combine into one payload - const payload = { - stickySession: stickySessionData.stickySession, - ssl: tlsData.ssl, - tlsServerName: tlsData.tlsServerName || null, - setHostHeader: proxyData.setHostHeader || null, - headers: proxyData.headers || null - }; - - // Single API call to update all settings - await api.post(`/resource/${resource.resourceId}`, payload); - - // Update local resource context + if (res && res.status === 200) { updateResource({ ...resource, - stickySession: stickySessionData.stickySession, - ssl: tlsData.ssl, - tlsServerName: tlsData.tlsServerName || null, - setHostHeader: proxyData.setHostHeader || null, - headers: proxyData.headers || null + stickySession: data.stickySession, + ssl: data.ssl, + tlsServerName: data.tlsServerName || null, + setHostHeader: data.setHostHeader || null, + headers: data.headers || null }); toast({ @@ -225,16 +182,6 @@ function ProxyResourceHttpForm({ }); router.refresh(); - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("settingsErrorUpdate"), - description: formatAxiosError( - err, - t("settingsErrorUpdateDescription") - ) - }); } } @@ -248,155 +195,158 @@ function ProxyResourceHttpForm({ {t("proxyAdditionalDescription")} + - -
- - {!env.flags.usePangolinDns && ( - ( - - - + + + + {!env.flags.usePangolinDns && ( + + ( + + + + + + )} + /> + + )} + + + ( + + + {t("targetTlsSni")} + + + + + + {t( + "targetTlsSniDescription" )} - defaultChecked={field.value} - onCheckedChange={(val) => { - field.onChange(val); - }} - /> - - - )} - /> - )} - ( - - - {t("targetTlsSni")} - - - - - - {t("targetTlsSniDescription")} - - - - )} - /> - - -
+ + + + )} + /> + - -
- - ( - - - { - field.onChange(val); - }} - /> - - - )} - /> - - -
+ + ( + + + + + + )} + /> + - -
- - ( - - - {t("proxyCustomHeader")} - - - - - - {t("proxyCustomHeaderDescription")} - - - - )} - /> - ( - - - {t("customHeaders")} - - - { - field.onChange(value); - }} - rows={4} - /> - - - {t("customHeadersDescription")} - - - - )} - /> + + ( + + + {t("proxyCustomHeader")} + + + + + + {t( + "proxyCustomHeaderDescription" + )} + + + + )} + /> + + + + ( + + + {t("customHeaders")} + + + + + + {t( + "customHeadersDescription" + )} + + + + )} + /> + +
-
- -
+ + + + ); -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/maintenance/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/maintenance/page.tsx index 39357105c..bb3dd7186 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/maintenance/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/maintenance/page.tsx @@ -15,13 +15,18 @@ import { Textarea } from "@/components/ui/textarea"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { SettingsContainer, + SettingsFormCell, + SettingsFormGrid, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, - SettingsSectionTitle + SettingsSectionTitle, + SettingsSubsectionDescription, + SettingsSubsectionHeader, + SettingsSubsectionTitle } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; import { useEnvContext } from "@app/hooks/useEnvContext"; @@ -37,12 +42,10 @@ 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"; + StrategySelect, + type StrategyOption +} from "@app/components/StrategySelect"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; @@ -158,6 +161,25 @@ export default function ResourceMaintenancePage() { return null; } + const isMaintenanceDisabled = !isPaidUser(tierMatrix.maintencePage); + + const maintenanceModeTypeOptions: StrategyOption< + "automatic" | "forced" + >[] = [ + { + id: "automatic", + title: `${t("automatic")} (${t("recommended")})`, + description: t("automaticModeDescription"), + disabled: isMaintenanceDisabled + }, + { + id: "forced", + title: t("forced"), + description: t("forcedModeDescription"), + disabled: isMaintenanceDisabled + } + ]; + return ( @@ -172,255 +194,237 @@ export default function ResourceMaintenancePage() { - +
- { - const isDisabled = !isPaidUser( - tierMatrix.maintencePage - ); + + + { + const isDisabled = !isPaidUser( + tierMatrix.maintencePage + ); - return ( - -
- - - - + + { + if ( + !isDisabled + ) { + maintenanceForm.setValue( + "maintenanceModeEnabled", + val + ); + } + }} + /> + + + + ); + }} + /> + + + {isMaintenanceEnabled && ( + <> + + ( + + + {t( + "maintenanceModeType" + )} + + + -
- { - if ( - !isDisabled - ) { - maintenanceForm.setValue( - "maintenanceModeEnabled", - val - ); - } - }} - /> -
-
-
-
-
-
- - {t( - "enableMaintenanceModeDescription" + value={ + field.value + } + options={ + maintenanceModeTypeOptions + } + onChange={ + field.onChange + } + cols={2} + /> + + +
)} - - - - ); - }} - /> + /> +
- {isMaintenanceEnabled && ( -
- ( - - + {maintenanceModeType === + "forced" && ( + + + + + {t( + "forcedeModeWarning" + )} + + + + )} + + + + {t( - "maintenanceModeType" + "maintenancePageContentSubsection" )} - - - - - - - -
- - - {t( - "automatic" - )} - {" "} - ( - {t( - "recommended" - )} + + + {t( + "maintenancePageContentSubsectionDescription" + )} + + + + + + ( + + + {t("pageTitle")} + + + - - {t( - "automaticModeDescription" - )} - -
-
- - - - -
- - - {t( - "forced" - )} - - - - {t( - "forcedModeDescription" - )} - -
-
-
-
- -
- )} - /> + } + placeholder="We'll be back soon!" + /> + + + {t( + "pageTitleDescription" + )} + + + + )} + /> + - {maintenanceModeType === "forced" && ( - - - - {t("forcedeModeWarning")} - - - )} + + ( + + + {t( + "maintenancePageMessage" + )} + + +