fix: rdp/vnc dupliocated icon, mobile hover fix for menu, and guacd hosts not working when ssh is enabeld

This commit is contained in:
LukeGus
2026-05-27 19:11:05 -05:00
parent 4816844bf0
commit e421a2ccc5
7 changed files with 79 additions and 16 deletions
+26 -1
View File
@@ -144,6 +144,17 @@ router.post("/token", async (req, res) => {
* schema: * schema:
* type: integer * type: integer
* description: Host ID to connect to * description: Host ID to connect to
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* protocol:
* type: string
* enum: [rdp, vnc, telnet]
* description: Override the host's default connection type
* responses: * responses:
* 200: * 200:
* description: Connection token generated successfully * description: Connection token generated successfully
@@ -205,13 +216,27 @@ router.post(
} }
} }
const connectionType = (host.connectionType as string) || "ssh"; const requestedProtocol = req.body?.protocol as string | undefined;
const connectionType =
requestedProtocol || (host.connectionType as string);
if (!["rdp", "vnc", "telnet"].includes(connectionType)) { if (!["rdp", "vnc", "telnet"].includes(connectionType)) {
return res.status(400).json({ return res.status(400).json({
error: `Connection type '${connectionType}' is not supported for remote desktop. Only RDP, VNC, and Telnet are supported.`, error: `Connection type '${connectionType}' is not supported for remote desktop. Only RDP, VNC, and Telnet are supported.`,
}); });
} }
const protocolEnabledMap: Record<string, boolean> = {
rdp: !!host.enableRdp,
vnc: !!host.enableVnc,
telnet: !!host.enableTelnet,
};
if (!protocolEnabledMap[connectionType]) {
return res.status(400).json({
error: `${connectionType.toUpperCase()} is not enabled for this host.`,
});
}
let guacConfig: Record<string, unknown> = {}; let guacConfig: Record<string, unknown> = {};
if (host.guacamoleConfig) { if (host.guacamoleConfig) {
try { try {
+25 -6
View File
@@ -15,9 +15,14 @@ import type { SSHHost } from "@/types";
interface GuacamoleAppProps { interface GuacamoleAppProps {
hostId?: string; hostId?: string;
tabId?: string; tabId?: string;
protocol?: "rdp" | "vnc" | "telnet";
} }
const GuacamoleApp: React.FC<GuacamoleAppProps> = ({ hostId, tabId }) => { const GuacamoleApp: React.FC<GuacamoleAppProps> = ({
hostId,
tabId,
protocol,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -76,6 +81,7 @@ const GuacamoleApp: React.FC<GuacamoleAppProps> = ({ hostId, tabId }) => {
hostId={parseInt(hostId, 10)} hostId={parseInt(hostId, 10)}
hostConfig={hostConfig} hostConfig={hostConfig}
tabId={tabId} tabId={tabId}
protocol={protocol}
/> />
); );
}} }}
@@ -87,12 +93,14 @@ interface GuacamoleAppInnerProps {
hostId: number; hostId: number;
hostConfig: Pick<SSHHost, "connectionType">; hostConfig: Pick<SSHHost, "connectionType">;
tabId?: string; tabId?: string;
protocol?: "rdp" | "vnc" | "telnet";
} }
const GuacamoleAppInner: React.FC<GuacamoleAppInnerProps> = ({ const GuacamoleAppInner: React.FC<GuacamoleAppInnerProps> = ({
hostId, hostId,
hostConfig, hostConfig,
tabId, tabId,
protocol,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [token, setToken] = useState<string | null>(null); const [token, setToken] = useState<string | null>(null);
@@ -110,7 +118,7 @@ const GuacamoleAppInner: React.FC<GuacamoleAppInnerProps> = ({
setError(t("guacamole.guacdUnavailable")); setError(t("guacamole.guacdUnavailable"));
return; return;
} }
return getGuacamoleTokenFromHost(hostId); return getGuacamoleTokenFromHost(hostId, protocol);
}) })
.then((result) => { .then((result) => {
if (result) setToken(result.token); if (result) setToken(result.token);
@@ -172,14 +180,21 @@ const GuacamoleAppInner: React.FC<GuacamoleAppInnerProps> = ({
<SimpleLoader <SimpleLoader
visible={true} visible={true}
message={t("guacamole.connecting", { message={t("guacamole.connecting", {
type: (hostConfig.connectionType || "remote").toUpperCase(), type: (
protocol ||
hostConfig.connectionType ||
"remote"
).toUpperCase(),
})} })}
/> />
</div> </div>
); );
} }
const protocol = hostConfig.connectionType as "rdp" | "vnc" | "telnet"; const resolvedProtocol = (protocol ?? hostConfig.connectionType) as
| "rdp"
| "vnc"
| "telnet";
return ( return (
<div className="relative w-full h-full"> <div className="relative w-full h-full">
@@ -213,13 +228,17 @@ const GuacamoleAppInner: React.FC<GuacamoleAppInnerProps> = ({
<GuacamoleDisplay <GuacamoleDisplay
key={token} key={token}
ref={displayRef} ref={displayRef}
connectionConfig={{ token, protocol, type: protocol }} connectionConfig={{
token,
protocol: resolvedProtocol,
type: resolvedProtocol,
}}
isVisible={true} isVisible={true}
onError={(err) => setConnectionError(err)} onError={(err) => setConnectionError(err)}
/> />
<GuacamoleToolbar <GuacamoleToolbar
displayRef={displayRef} displayRef={displayRef}
protocol={protocol} protocol={resolvedProtocol}
onReconnect={handleReconnect} onReconnect={handleReconnect}
/> />
</div> </div>
+5 -1
View File
@@ -4360,9 +4360,13 @@ export async function getGuacamoleToken(
export async function getGuacamoleTokenFromHost( export async function getGuacamoleTokenFromHost(
hostId: number, hostId: number,
protocol?: "rdp" | "vnc" | "telnet",
): Promise<GuacamoleTokenResponse> { ): Promise<GuacamoleTokenResponse> {
try { try {
const response = await authApi.post(`/guacamole/connect-host/${hostId}`); const response = await authApi.post(
`/guacamole/connect-host/${hostId}`,
protocol ? { protocol } : {},
);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw handleApiError(error, "get guacamole token from host"); throw handleApiError(error, "get guacamole token from host");
+3 -2
View File
@@ -27,6 +27,7 @@ import {
KeyRound, KeyRound,
LayoutDashboard, LayoutDashboard,
Monitor, Monitor,
MousePointerClick,
Clock, Clock,
Folder, Folder,
Pencil, Pencil,
@@ -48,7 +49,7 @@ const ACTIVITY_ICONS: Record<string, React.ReactNode> = {
tunnel: <Network className="size-3.5" />, tunnel: <Network className="size-3.5" />,
docker: <Box className="size-3.5" />, docker: <Box className="size-3.5" />,
telnet: <MessagesSquare className="size-3.5" />, telnet: <MessagesSquare className="size-3.5" />,
vnc: <Monitor className="size-3.5" />, vnc: <MousePointerClick className="size-3.5" />,
rdp: <Monitor className="size-3.5" />, rdp: <Monitor className="size-3.5" />,
}; };
@@ -424,7 +425,7 @@ export function CommandPalette({
}} }}
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40" className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
> >
<Monitor className="size-3" /> <MousePointerClick className="size-3" />
VNC VNC
</button> </button>
)} )}
+7 -1
View File
@@ -230,7 +230,13 @@ export function renderTabContent(
return ( return (
<EmptyState icon={Monitor} messageKey="guacamole.noHostSelected" /> <EmptyState icon={Monitor} messageKey="guacamole.noHostSelected" />
); );
return <GuacamoleApp hostId={host.id} tabId={tab.id} />; return (
<GuacamoleApp
hostId={host.id}
tabId={tab.id}
protocol={tab.type as "rdp" | "vnc" | "telnet"}
/>
);
case "network_graph": case "network_graph":
return <NetworkGraphCard embedded={false} />; return <NetworkGraphCard embedded={false} />;
+3 -2
View File
@@ -31,6 +31,7 @@ import {
Lock, Lock,
Monitor, Monitor,
Network, Network,
MousePointerClick,
Palette, Palette,
Pencil, Pencil,
Plus, Plus,
@@ -220,7 +221,7 @@ function makeHostTabs(t: (key: string) => string): HostTab[] {
{ {
id: "vnc", id: "vnc",
label: t("hosts.tabVnc"), label: t("hosts.tabVnc"),
icon: <Monitor className="size-3" />, icon: <MousePointerClick className="size-3" />,
}, },
{ {
id: "telnet", id: "telnet",
@@ -687,7 +688,7 @@ function HostEditor({
proto: "enableVnc" as const, proto: "enableVnc" as const,
label: t("hosts.tabVnc"), label: t("hosts.tabVnc"),
desc: t("hosts.virtualNetwork"), desc: t("hosts.virtualNetwork"),
icon: <Monitor className="size-4" />, icon: <MousePointerClick className="size-4" />,
portField: "vncPort" as const, portField: "vncPort" as const,
}, },
{ {
+10 -3
View File
@@ -16,6 +16,7 @@ import {
MessagesSquare, MessagesSquare,
Monitor, Monitor,
MoreHorizontal, MoreHorizontal,
MousePointerClick,
Network, Network,
Pencil, Pencil,
Pin, Pin,
@@ -215,11 +216,17 @@ export function HostItem({
? "bg-muted/20" ? "bg-muted/20"
: "" : ""
} ${isMenuOpen ? "bg-muted/40" : ""}`} } ${isMenuOpen ? "bg-muted/40" : ""}`}
onClick={() => { onClick={(e) => {
if (selectionMode) { if (selectionMode) {
onToggleSelect?.(); onToggleSelect?.();
return; return;
} }
// On touch devices open the action tray instead of immediately launching a tab
if (window.matchMedia("(hover: none)").matches) {
e.stopPropagation();
onMenuOpenChange?.(!isMenuOpen);
return;
}
if (host.enableSsh) onOpenTab("terminal"); if (host.enableSsh) onOpenTab("terminal");
else if (host.enableRdp) onOpenTab("rdp"); else if (host.enableRdp) onOpenTab("rdp");
else if (host.enableVnc) onOpenTab("vnc"); else if (host.enableVnc) onOpenTab("vnc");
@@ -360,7 +367,7 @@ export function HostItem({
}} }}
className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors" className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors"
> >
<Monitor className="size-3.5" /> <MousePointerClick className="size-3.5" />
</button> </button>
)} )}
{host.enableTelnet && ( {host.enableTelnet && (
@@ -526,7 +533,7 @@ export function HostItem({
toast.success(t("hosts.vncUrlCopied")); toast.success(t("hosts.vncUrlCopied"));
}} }}
> >
<Monitor className="size-3.5 mr-2" /> <MousePointerClick className="size-3.5 mr-2" />
{t("hosts.copyVncUrlAction")} {t("hosts.copyVncUrlAction")}
</DropdownMenuItem> </DropdownMenuItem>
)} )}