From 4c1e1daf07f28deccc72e8ed278f296361a3212a Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 22 May 2026 17:37:37 -0700 Subject: [PATCH] All page types are there and look mostly correct --- messages/en-US.json | 10 +- .../resources/proxy/[niceId]/general/page.tsx | 118 +++++---- .../resources/proxy/[niceId]/layout.tsx | 18 +- .../resources/proxy/[niceId]/page.tsx | 2 +- .../resources/proxy/[niceId]/rdp/page.tsx | 250 ++++++++++++++++++ .../resources/proxy/[niceId]/ssh/page.tsx | 127 +++++---- .../resources/proxy/[niceId]/vnc/page.tsx | 248 +++++++++++++++++ .../settings/resources/proxy/create/page.tsx | 13 +- src/components/BrowserGatewayTargetForm.tsx | 5 +- src/components/ResourceInfoBox.tsx | 186 ++++++------- src/components/Settings.tsx | 15 +- 11 files changed, 767 insertions(+), 225 deletions(-) create mode 100644 src/app/[orgId]/settings/resources/proxy/[niceId]/rdp/page.tsx create mode 100644 src/app/[orgId]/settings/resources/proxy/[niceId]/vnc/page.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 41c9a6919..5bd4249d9 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1971,10 +1971,17 @@ "requireDeviceApproval": "Require Device Approvals", "requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.", "sshSettings": "SSH Settings", + "rdpSettings": "RDP Settings", + "vncSettings": "VNC Settings", "sshServer": "SSH Server", + "rdpServer": "RDP Server", + "vncServer": "VNC Server", "sshServerDescription": "Set up the authentication method, daemon location, and server destination", + "rdpServerDescription": "Configure the destination and port of the RDP server", + "vncServerDescription": "Configure the destination and port of the VNC server", "sshServerMode": "Mode", "sshServerModeStandard": "Standard SSH Server", + "sshServerModePangolin": "Pangolin SSH", "sshServerModeStandardDescription": "Uses a Pangolin auth daemon to manage SSH authentication on the site or remote host.", "sshServerModeNative": "Native SSH Server", "sshServerModeNativeDescription": "SSH authentication is handled natively by an existing SSH server without a separate auth daemon.", @@ -2987,7 +2994,7 @@ "learnMore": "Learn more", "backToHome": "Go back to home", "needToSignInToOrg": "Need to use your organization's identity provider?", - "maintenanceMode": "Maintenance Mode", + "maintenanceMode": "Maintenance Page", "maintenanceModeDescription": "Display a maintenance page to visitors", "maintenanceModeType": "Maintenance Mode Type", "showMaintenancePage": "Show a maintenance page to visitors", @@ -3017,6 +3024,7 @@ "maintenanceScreenEstimatedCompletion": "Estimated Completion:", "createInternalResourceDialogDestinationRequired": "Destination is required", "available": "Available", + "disabledResourceDescription": "When disabled, the resource will be inaccessible by everyone.", "archived": "Archived", "noArchivedDevices": "No archived devices found", "deviceArchived": "Device archived", diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index 11c347f36..3ff75cec4 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -584,43 +584,44 @@ export default function GeneralForm() { className="space-y-4" id="general-settings-form" > - ( - - - {t("name")} - - - - - - - )} - /> +
+ ( + + + {t("name")} + + + + + + + )} + /> - ( - - - {t("identifier")} - - - - - - - )} - /> + ( + + + {t("identifier")} + + + + + + + )} + /> +
{!resource.http && ( <> @@ -730,28 +731,31 @@ export default function GeneralForm() { control={form.control} name="enabled" render={() => ( - -
- - + + + form.setValue( + "enabled", val - ) => - form.setValue( - "enabled", - val - ) - } - /> - -
+ ) + } + /> + + + {t( + "disabledResourceDescription" + )} +
)} diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx index f013afa27..1d3a84db5 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx @@ -84,23 +84,13 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { { title: t("general"), href: `/{orgId}/settings/resources/proxy/{niceId}/general` + }, + { + title: t(`${resource.browserAccessType}Settings`), + href: `/{orgId}/settings/resources/proxy/{niceId}/${resource.browserAccessType}` } ]; - if (resource.browserAccessType === "http") { - navItems.push({ - title: t("httpSettings"), - href: `/{orgId}/settings/resources/proxy/{niceId}/http` - }); - } - - if (resource.browserAccessType === "ssh") { - navItems.push({ - title: t("sshSettings"), - href: `/{orgId}/settings/resources/proxy/{niceId}/ssh` - }); - } - if (resource.http) { navItems.push({ title: t("authentication"), diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/page.tsx index 06a4af045..10950bfca 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/page.tsx @@ -10,6 +10,6 @@ export default async function ResourcePage(props: { }) { const params = await props.params; redirect( - `/${params.orgId}/settings/resources/proxy/${params.niceId}/proxy` + `/${params.orgId}/settings/resources/proxy/${params.niceId}/general` ); } diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/rdp/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/rdp/page.tsx new file mode 100644 index 000000000..defd4891a --- /dev/null +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/rdp/page.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm"; +import { type Selectedsite } from "@app/components/site-selector"; +import { Button } from "@app/components/ui/button"; +import { toast } from "@app/hooks/useToast"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient } from "@app/lib/api"; +import { formatAxiosError } from "@app/lib/api/formatAxiosError"; +import { useQuery } from "@tanstack/react-query"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { use, useActionState, useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { GetResourceResponse } from "@server/routers/resource"; +import type { ResourceContextType } from "@app/contexts/resourceContext"; + +type ExistingTarget = { + browserGatewayTargetId: number; + siteId: number; +}; + +const sshFormSchema = z.object({ + authDaemonPort: z.string().refine( + (val) => { + if (!val) return true; + const n = Number(val); + return Number.isInteger(n) && n >= 1 && n <= 65535; + }, + { message: "Port must be between 1 and 65535" } + ) +}); + +export default function SshSettingsPage(props: { + params: Promise<{ orgId: string }>; +}) { + const params = use(props.params); + const { resource, updateResource } = useResourceContext(); + + return ( + + + + ); +} + +function SshServerForm({ + orgId, + resource, + updateResource +}: { + orgId: string; + resource: GetResourceResponse; + updateResource: ResourceContextType["updateResource"]; +}) { + const t = useTranslations(); + const api = createApiClient(useEnvContext()); + const router = useRouter(); + + // Standard mode: multi-site + const [selectedSites, setSelectedSites] = useState([]); + const [bgDestination, setBgDestination] = useState(""); + const [bgDestinationPort, setBgDestinationPort] = useState("22"); + const [existingTargets, setExistingTargets] = useState( + [] + ); + + // Native mode: single site + const [selectedNativeSite, setSelectedNativeSite] = + useState(null); + const [nativeExistingTarget, setNativeExistingTarget] = + useState(null); + + const { data: bgTargetsResponse } = useQuery({ + queryKey: ["browserGatewayTargets", resource.resourceId, orgId], + queryFn: async () => { + const res = await api.get( + `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-targets` + ); + return res.data.data as { + targets: Array<{ + browserGatewayTargetId: number; + resourceId: number; + siteId: number; + siteName?: string; + type: string; + destination: string; + destinationPort: number; + }>; + }; + } + }); + + useEffect(() => { + if (!bgTargetsResponse?.targets?.length) return; + const targets = bgTargetsResponse.targets; + const first = targets[0]; + + setBgDestination(first.destination); + setBgDestinationPort(String(first.destinationPort)); + setExistingTargets( + targets.map((t) => ({ + browserGatewayTargetId: t.browserGatewayTargetId, + siteId: t.siteId + })) + ); + setSelectedSites( + targets.map((t) => ({ + siteId: t.siteId, + name: t.siteName ?? String(t.siteId), + type: "newt" as const + })) + ); + }, [bgTargetsResponse]); + + const [, formAction, isSubmitting] = useActionState(save, null); + + async function save() { + try { + if (bgDestination && bgDestinationPort) { + const selectedSiteIds = new Set( + selectedSites.map((s) => s.siteId) + ); + const existingSiteIds = new Set( + existingTargets.map((t) => t.siteId) + ); + + const toDelete = existingTargets.filter( + (t) => !selectedSiteIds.has(t.siteId) + ); + await Promise.all( + toDelete.map((t) => + api.delete( + `/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}` + ) + ) + ); + + const toUpdate = existingTargets.filter((t) => + selectedSiteIds.has(t.siteId) + ); + await Promise.all( + toUpdate.map((t) => + api.post( + `/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}`, + { + type: "rdp", + destination: bgDestination, + destinationPort: Number(bgDestinationPort), + siteId: t.siteId + } + ) + ) + ); + + const toCreate = selectedSites.filter( + (s) => !existingSiteIds.has(s.siteId) + ); + const created = await Promise.all( + toCreate.map((s) => + api.put( + `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`, + { + siteId: s.siteId, + type: "rdp", + destination: bgDestination, + destinationPort: Number(bgDestinationPort) + } + ) + ) + ); + + const newTargets: ExistingTarget[] = created.map((res, i) => ({ + browserGatewayTargetId: + res.data.data.browserGatewayTargetId, + siteId: toCreate[i].siteId + })); + setExistingTargets([...toUpdate, ...newTargets]); + } + + toast({ + title: t("settingsUpdated"), + description: t("settingsUpdatedDescription") + }); + router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("settingsErrorUpdate"), + description: formatAxiosError( + err, + t("settingsErrorUpdateDescription") + ) + }); + } + } + + return ( + + + {t("rdpServer")} + + {t("rdpServerDescription")} + + + + + + + +
+ +
+
+ ); +} diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/ssh/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/ssh/page.tsx index 2f874bd53..cc733d6ab 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/ssh/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/ssh/page.tsx @@ -31,6 +31,7 @@ import { PopoverTrigger } from "@app/components/ui/popover"; import { ChevronsUpDown, ExternalLink } from "lucide-react"; +import { Badge } from "@app/components/ui/badge"; import { toast } from "@app/hooks/useToast"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { useEnvContext } from "@app/hooks/useEnvContext"; @@ -122,6 +123,7 @@ function SshServerForm({ // Standard mode: multi-site const [selectedSites, setSelectedSites] = useState([]); + const [selectedSite, setSelectedSite] = useState(null); const [bgDestination, setBgDestination] = useState(""); const [bgDestinationPort, setBgDestinationPort] = useState("22"); const [existingTargets, setExistingTargets] = useState( @@ -365,11 +367,16 @@ function SshServerForm({ - +

{t("sshServerMode")}

+ + {sshServerMode == "standard" + ? t("sshServerModeStandard") + : t("sshServerModePangolin")} +
@@ -434,60 +441,74 @@ function SshServerForm({ /> )} - -
-
-

- {t("sshServerDestination")} -

-

- {t("sshServerDestinationDescription")} -

+
+
+

+ {t("sshServerDestination")} +

+

+ {t("sshServerDestinationDescription")} +

+
+ {isNative ? ( + + + + + + { + setSelectedNativeSite(site); + setNativeSiteOpen(false); + }} + /> + + + ) : standardDaemonLocation !== "site" ? ( + + ) : ( + + )}
- {isNative ? ( - - - - - - { - setSelectedNativeSite(site); - setNativeSiteOpen(false); - }} - /> - - - ) : ( - - )} -
+
+
+ + ); +} diff --git a/src/app/[orgId]/settings/resources/proxy/create/page.tsx b/src/app/[orgId]/settings/resources/proxy/create/page.tsx index d69bbdcf0..2a062987f 100644 --- a/src/app/[orgId]/settings/resources/proxy/create/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/create/page.tsx @@ -713,7 +713,7 @@ export default function Page() { } }; - return ( + return (
{row.original.siteType === "newt" ? ( - ) : ( - )} @@ -1427,10 +1426,12 @@ export default function Page() { )} {build === "saas" && targets.length > 1 && - new Set(targets.map((t) => t.siteId)).size > - 1 && ( + new Set(targets.map((t) => t.siteId)) + .size > 1 && (

- {t("proxyMultiSiteRoundRobinNodeHelp")}{" "} + {t( + "proxyMultiSiteRoundRobinNodeHelp" + )}{" "} router.push( - `/${orgId}/settings/resources/proxy/${niceId}/proxy` + `/${orgId}/settings/resources/proxy/${niceId}` ) } > diff --git a/src/components/BrowserGatewayTargetForm.tsx b/src/components/BrowserGatewayTargetForm.tsx index cbc5cfeed..55f74aa27 100644 --- a/src/components/BrowserGatewayTargetForm.tsx +++ b/src/components/BrowserGatewayTargetForm.tsx @@ -27,6 +27,7 @@ type MultiSiteProps = { export type BrowserGatewayTargetFormProps = { orgId: string; destination: string; + defaultPort: number; destinationPort: string; onDestinationChange: (v: string) => void; onDestinationPortChange: (v: string) => void; @@ -115,7 +116,7 @@ export function BrowserGatewayTargetForm(props: BrowserGatewayTargetFormProps) { props.onDestinationPortChange(e.target.value) @@ -123,7 +124,7 @@ export function BrowserGatewayTargetForm(props: BrowserGatewayTargetFormProps) { />

- {props.multiSite && ( + {props.multiSite === true && props.selectedSites.length > 1 && (

{t("bgTargetMultiSiteDisclaimer")}{" "} - {/* 4 cols because of the certs */} - - + + {/* {t("identifier")} {resource.niceId} - + */} {resource.http ? ( <> @@ -62,6 +81,18 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { )} + {showType && ( + + + {t("type")} + + + + {resource.browserAccessType!.toUpperCase()} + + + + )} {t("authentication")} @@ -84,24 +115,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { )} - {/* {isEnabled && ( - - Socket - - {isAvailable ? ( - -

- Online - - ) : ( - -
- Offline -
- )} - - - )} */} ) : ( <> @@ -149,74 +162,69 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { {/* */} {/* */} {/* Certificate Status Column */} - {resource.http && - resource.domainId && - resource.fullDomain && - build != "oss" && ( - - - {t("certificateStatus", { - defaultValue: "Certificate" - })} - - - - - - )} - - {t("health")} - - {resource.health === "healthy" && ( -
- - {t("resourcesTableHealthy")} -
- )} - {resource.health === "degraded" && ( -
- - {t("resourcesTableDegraded")} -
- )} - {resource.health === "unhealthy" && ( -
- - {t("resourcesTableUnhealthy")} -
- )} - {(!resource.health || - resource.health === "unknown") && ( -
- - {t("resourcesTableUnknown")} -
- )} -
-
- - {t("visibility")} - - {resource.enabled ? ( -
- - {t("enabled")} -
- ) : ( + {showCertificate && ( + + + {t("certificateStatus", { + defaultValue: "Certificate" + })} + + + + + + )} + {showHealth && ( + + {t("health")} + + {resource.health === "healthy" && ( +
+ + + {t("resourcesTableHealthy")} + +
+ )} + {resource.health === "degraded" && ( +
+ + + {t("resourcesTableDegraded")} + +
+ )} + {resource.health === "unhealthy" && ( +
+ + + {t("resourcesTableUnhealthy")} + +
+ )} +
+
+ )} + {showVisibility && ( + + + {t("visibility")} + +
{t("disabled")}
- )} -
-
+
+
+ )} diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index e3138550b..87c7c2db3 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -22,13 +22,24 @@ export function SettingsSectionHeader({ export function SettingsSectionForm({ children, - className + className, + variant = "compact" }: { children: React.ReactNode; + variant?: "half" | "compact"; className?: string; }) { return ( -
{children}
+
+ {children} +
); }