diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 16a82e400..e76fbbf05 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -1,4 +1,5 @@ import { + browserGatewayTarget, db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility, @@ -433,6 +434,30 @@ export async function listResources( ) .leftJoin(sites, eq(targets.siteId, sites.siteId)); + const allBgTargetSites = + resourceIdList.length === 0 + ? [] + : await db + .select({ + resourceId: browserGatewayTarget.resourceId, + siteId: browserGatewayTarget.siteId, + siteName: sites.name, + siteNiceId: sites.niceId, + siteOnline: sites.online, + siteType: sites.type + }) + .from(browserGatewayTarget) + .where( + inArray( + browserGatewayTarget.resourceId, + resourceIdList + ) + ) + .leftJoin( + sites, + eq(sites.siteId, browserGatewayTarget.siteId) + ); + // avoids TS issues with reduce/never[] const map = new Map(); @@ -493,6 +518,21 @@ export async function listResources( online: isLocal ? undefined : Boolean(t.siteOnline) }); } + const bgRaw = allBgTargetSites.filter( + (t) => t.resourceId === entry.resourceId + ); + for (const t of bgRaw) { + if (typeof t.siteId !== "number" || siteById.has(t.siteId)) { + continue; + } + const isLocal = t.siteType === "local"; + siteById.set(t.siteId, { + siteId: t.siteId, + siteName: t.siteName ?? "", + siteNiceId: t.siteNiceId ?? "", + online: isLocal ? undefined : Boolean(t.siteOnline) + }); + } entry.sites = Array.from(siteById.values()); } diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index 823c0f957..2ac4eafe4 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -201,6 +201,14 @@ function ProxyResourceTargetsForm({ const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = useState(null); + const [targetMode, setTargetMode] = useState< + "http" | "ssh" | "rdp" | "vnc" + >("http"); + const [bgDestination, setBgDestination] = useState(""); + const [bgDestinationPort, setBgDestinationPort] = useState(""); + const [bgSiteId, setBgSiteId] = useState(null); + const [bgTargetId, setBgTargetId] = useState(null); + const initializeDockerForSite = async (siteId: number) => { if (dockerStates.has(siteId)) { return; // Already initialized @@ -270,6 +278,41 @@ function ProxyResourceTargetsForm({ }) ); + 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; + type: string; + destination: string; + destinationPort: number; + }>; + }; + } + }); + + useEffect(() => { + if (!bgTargetsResponse?.targets?.length) return; + const bgt = bgTargetsResponse.targets[0]; + setTargetMode(bgt.type as "ssh" | "rdp" | "vnc"); + setBgDestination(bgt.destination); + setBgDestinationPort(String(bgt.destinationPort)); + setBgSiteId(bgt.siteId); + setBgTargetId(bgt.browserGatewayTargetId); + }, [bgTargetsResponse]); + + useEffect(() => { + if (sites.length > 0 && bgSiteId === null) { + setBgSiteId(sites[0].siteId); + } + }, [sites, bgSiteId]); + const updateTarget = useCallback( (targetId: number, data: Partial) => { setTargets((prevTargets) => { @@ -356,7 +399,7 @@ function ProxyResourceTargetsForm({ } }; - return ( + return (
{row.original.siteType === "newt" ? ( - ) : ( - )} @@ -404,9 +446,15 @@ function ProxyResourceTargetsForm({ pathMatchType: row.original.pathMatchType }} onChange={(config) => - updateTarget(row.original.targetId, - config.path === null && config.pathMatchType === null - ? { ...config, rewritePath: null, rewritePathType: null } + updateTarget( + row.original.targetId, + config.path === null && + config.pathMatchType === null + ? { + ...config, + rewritePath: null, + rewritePathType: null + } : config ) } @@ -432,9 +480,15 @@ function ProxyResourceTargetsForm({ pathMatchType: row.original.pathMatchType }} onChange={(config) => - updateTarget(row.original.targetId, - config.path === null && config.pathMatchType === null - ? { ...config, rewritePath: null, rewritePathType: null } + updateTarget( + row.original.targetId, + config.path === null && + config.pathMatchType === null + ? { + ...config, + rewritePath: null, + rewritePathType: null + } : config ) } @@ -717,6 +771,55 @@ function ProxyResourceTargetsForm({ const [, formAction, isSubmitting] = useActionState(saveTargets, null); async function saveTargets() { + if (targetMode !== "http") { + try { + if (!bgDestination || !bgDestinationPort) { + if (bgTargetId) { + await api.delete( + `/org/${orgId}/browser-gateway-target/${bgTargetId}` + ); + setBgTargetId(null); + } + } else if (bgTargetId) { + await api.post( + `/org/${orgId}/browser-gateway-target/${bgTargetId}`, + { + type: targetMode, + destination: bgDestination, + destinationPort: Number(bgDestinationPort), + siteId: bgSiteId + } + ); + } else { + const res = await api.put( + `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`, + { + siteId: bgSiteId ?? sites[0]?.siteId, + type: targetMode, + destination: bgDestination, + destinationPort: Number(bgDestinationPort) + } + ); + setBgTargetId(res.data.data.browserGatewayTargetId); + } + toast({ + title: t("settingsUpdated"), + description: t("settingsUpdatedDescription") + }); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("settingsErrorUpdate"), + description: formatAxiosError( + err, + t("settingsErrorUpdateDescription") + ) + }); + } + return; + } + // Validate that no targets have blank IPs or invalid ports const targetsWithInvalidFields = targets.filter( (target) => @@ -791,12 +894,14 @@ function ProxyResourceTargetsForm({ } toast({ - title: targets.length === 0 - ? t("targetTargetsCleared") - : t("settingsUpdated"), - description: targets.length === 0 - ? t("targetTargetsClearedDescription") - : t("settingsUpdatedDescription") + title: + targets.length === 0 + ? t("targetTargetsCleared") + : t("settingsUpdated"), + description: + targets.length === 0 + ? t("targetTargetsClearedDescription") + : t("settingsUpdatedDescription") }); setTargetsToRemove([]); @@ -829,102 +934,168 @@ function ProxyResourceTargetsForm({ - {targets.length > 0 ? ( +
+ Target Type + +
+ {targetMode === "http" ? ( <> -
- - - {table - .getHeaderGroups() - .map((headerGroup) => ( - - {headerGroup.headers.map( - (header) => { - const isActionsColumn = - header.column - .id === - "actions"; - return ( - - {header.isPlaceholder - ? null - : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} - - ); - } - )} - - ))} - - - {table.getRowModel().rows?.length ? ( - table - .getRowModel() - .rows.map((row) => ( - - {row - .getVisibleCells() - .map((cell) => { - const isActionsColumn = - cell.column - .id === - "actions"; - return ( - - {flexRender( + {targets.length > 0 ? ( + <> +
+
+ + {table + .getHeaderGroups() + .map((headerGroup) => ( + + {headerGroup.headers.map( + (header) => { + const isActionsColumn = + header + .column + .id === + "actions"; + return ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ); + } + )} + + ))} + + + {table.getRowModel().rows + ?.length ? ( + table + .getRowModel() + .rows.map((row) => ( + + {row + .getVisibleCells() + .map( + ( cell - .column - .columnDef - .cell, - cell.getContext() - )} - - ); - })} + ) => { + const isActionsColumn = + cell + .column + .id === + "actions"; + return ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext() + )} + + ); + } + )} + + )) + ) : ( + + + {t("targetNoOne")} + - )) - ) : ( - - + {/* */} + {/* {t('targetNoOneDescription')} */} + {/* */} +
+
+
+
+ +
+ +
-
-
+ {t("advancedMode")} + +
+
+
+ + ) : ( +
+

+ {t("targetNoOne")} +

-
- - -
-
+ )} + {build === "saas" && + targets.length > 1 && + new Set(targets.map((t) => t.siteId)).size > + 1 && ( +

+ {t("proxyMultiSiteRoundRobinNodeHelp")}{" "} + + {t("learnMore")} + + + . +

+ )} ) : ( -
-

- {t("targetNoOne")} -

- +
+
+
+ + + setBgDestination(e.target.value) + } + /> +
+
+ + + setBgDestinationPort(e.target.value) + } + /> +
+
+ {sites.length > 1 && ( +
+ + +
+ )}
)} - {build === "saas" && - targets.length > 1 && - new Set(targets.map((t) => t.siteId)).size > 1 && ( -

- {t("proxyMultiSiteRoundRobinNodeHelp")}{" "} - - {t("learnMore")} - - - . -

- )}
diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index 785387fad..690591c3d 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -322,9 +322,7 @@ export default function RdpClient({
{showLogin && (
-

- RDP Test Connection -

+

RDP

diff --git a/src/app/ssh/SshClient.tsx b/src/app/ssh/SshClient.tsx index 1fd388b47..8eddd97b7 100644 --- a/src/app/ssh/SshClient.tsx +++ b/src/app/ssh/SshClient.tsx @@ -244,9 +244,7 @@ export default function SshClient({
{!connected && (
-

- SSH Terminal -

+

SSH

diff --git a/src/app/vnc/VncClient.tsx b/src/app/vnc/VncClient.tsx index 17593d6ba..c48472b8a 100644 --- a/src/app/vnc/VncClient.tsx +++ b/src/app/vnc/VncClient.tsx @@ -161,9 +161,7 @@ export default function VncClient({
{!connected && (
-

- VNC Test Connection -

+

VNC