mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
feat: quick connect for RDP and VNC (#1335)
The Quick Connect panel gets a protocol switch. RDP/VNC quick hosts are built like SSH ones (never saved) and opened as regular remote desktop tabs; GuacamoleApp mints their token from the typed fields through the existing /guacamole/token endpoint instead of a host-row lookup.
This commit is contained in:
@@ -21,7 +21,7 @@ import {
|
|||||||
isElectron,
|
isElectron,
|
||||||
} from "@/main-axios.ts";
|
} from "@/main-axios.ts";
|
||||||
import { readConfiguredDimension } from "@/features/guacamole/guacamole-display-size.ts";
|
import { readConfiguredDimension } from "@/features/guacamole/guacamole-display-size.ts";
|
||||||
import { parseGuacamoleConfig } from "@/api/guacamole-api";
|
import { getGuacamoleToken, parseGuacamoleConfig } from "@/api/guacamole-api";
|
||||||
import { resolveConnectionOrigin } from "@/lib/connection-origin.ts";
|
import { resolveConnectionOrigin } from "@/lib/connection-origin.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { GuacamoleToolbar } from "@/features/guacamole/GuacamoleToolbar.tsx";
|
import { GuacamoleToolbar } from "@/features/guacamole/GuacamoleToolbar.tsx";
|
||||||
@@ -52,8 +52,15 @@ interface GuacamoleAppProps {
|
|||||||
tabId?: string;
|
tabId?: string;
|
||||||
protocol?: "rdp" | "vnc" | "telnet";
|
protocol?: "rdp" | "vnc" | "telnet";
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
|
/** A quick-connect host: never saved, so the token is minted from its fields. */
|
||||||
|
quickConnectHost?: GuacamoleQuickHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What GuacamoleApp needs from a host that has no database row. */
|
||||||
|
export type GuacamoleQuickHost = GuacamoleAppInnerProps["hostConfig"] & {
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface GuacamoleAppHandle {
|
export interface GuacamoleAppHandle {
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
isConnected: () => boolean;
|
isConnected: () => boolean;
|
||||||
@@ -62,14 +69,31 @@ export interface GuacamoleAppHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
|
const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
|
||||||
function GuacamoleApp({ hostId, tabId, protocol, isVisible = true }, ref) {
|
function GuacamoleApp(
|
||||||
|
{ hostId, tabId, protocol, isVisible = true, quickConnectHost },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const defaults = useConnectionDefaults();
|
const defaults = useConnectionDefaults();
|
||||||
const [hostConfig, setHostConfig] = useState<SSHHost | null>(null);
|
const [hostConfig, setHostConfig] = useState<GuacamoleQuickHost | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!defaults.ready) return;
|
if (!defaults.ready) return;
|
||||||
|
if (quickConnectHost) {
|
||||||
|
const connectionType = protocol ?? quickConnectHost.connectionType;
|
||||||
|
setHostConfig({
|
||||||
|
...quickConnectHost,
|
||||||
|
guacamoleConfig: resolveConnectionDefaults(
|
||||||
|
connectionType === "rdp" ? defaults.rdp : {},
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!hostId) {
|
if (!hostId) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -93,7 +117,7 @@ const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
|
|||||||
})
|
})
|
||||||
.catch(() => setHostConfig(null))
|
.catch(() => setHostConfig(null))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [hostId, protocol, defaults.ready, defaults.rdp]);
|
}, [hostId, protocol, defaults.ready, defaults.rdp, quickConnectHost]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -117,9 +141,9 @@ const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
|
|||||||
return (
|
return (
|
||||||
<ConnectionLogProvider>
|
<ConnectionLogProvider>
|
||||||
<GuacamoleAppInner
|
<GuacamoleAppInner
|
||||||
hostId={parseInt(hostId, 10)}
|
hostId={quickConnectHost ? 0 : parseInt(hostId, 10)}
|
||||||
hostConfig={hostConfig}
|
hostConfig={hostConfig}
|
||||||
hostName={hostConfig.name || hostConfig.ip || String(hostId)}
|
hostName={hostConfig.name || hostConfig.ip || String(hostId ?? "")}
|
||||||
tabId={tabId}
|
tabId={tabId}
|
||||||
protocol={protocol}
|
protocol={protocol}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
@@ -134,7 +158,18 @@ interface GuacamoleAppInnerProps {
|
|||||||
hostId: number;
|
hostId: number;
|
||||||
hostConfig: Pick<
|
hostConfig: Pick<
|
||||||
SSHHost,
|
SSHHost,
|
||||||
"connectionType" | "domain" | "guacamoleConfig" | "rdpAuthType" | "syncId"
|
| "connectionType"
|
||||||
|
| "domain"
|
||||||
|
| "guacamoleConfig"
|
||||||
|
| "rdpAuthType"
|
||||||
|
| "syncId"
|
||||||
|
| "ip"
|
||||||
|
| "rdpPort"
|
||||||
|
| "vncPort"
|
||||||
|
| "rdpUser"
|
||||||
|
| "rdpPassword"
|
||||||
|
| "vncUser"
|
||||||
|
| "vncPassword"
|
||||||
>;
|
>;
|
||||||
hostName: string;
|
hostName: string;
|
||||||
tabId?: string;
|
tabId?: string;
|
||||||
@@ -253,7 +288,31 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
type: resolvedProtocolForConnect.toUpperCase(),
|
type: resolvedProtocolForConnect.toUpperCase(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const result = await getGuacamoleTokenFromHost(
|
// hostId 0 is a quick-connect host: nothing to look up, mint the token
|
||||||
|
// straight from what the user typed. It cannot be shared or logged as
|
||||||
|
// host activity because there is no host row.
|
||||||
|
const result =
|
||||||
|
hostId === 0
|
||||||
|
? await getGuacamoleToken({
|
||||||
|
protocol: resolvedProtocolForConnect,
|
||||||
|
hostname: hostConfig.ip,
|
||||||
|
port:
|
||||||
|
resolvedProtocolForConnect === "vnc"
|
||||||
|
? hostConfig.vncPort
|
||||||
|
: hostConfig.rdpPort,
|
||||||
|
username:
|
||||||
|
resolvedProtocolForConnect === "vnc"
|
||||||
|
? hostConfig.vncUser
|
||||||
|
: hostConfig.rdpUser,
|
||||||
|
password:
|
||||||
|
resolvedProtocolForConnect === "vnc"
|
||||||
|
? hostConfig.vncPassword
|
||||||
|
: hostConfig.rdpPassword,
|
||||||
|
domain: hostConfig.domain,
|
||||||
|
ignoreCert: true,
|
||||||
|
guacamoleConfig: parseGuacamoleConfig(hostConfig.guacamoleConfig),
|
||||||
|
})
|
||||||
|
: await getGuacamoleTokenFromHost(
|
||||||
hostId,
|
hostId,
|
||||||
protocol,
|
protocol,
|
||||||
promptedCredentials ?? undefined,
|
promptedCredentials ?? undefined,
|
||||||
@@ -262,7 +321,11 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
if (result) {
|
if (result) {
|
||||||
setToken(result.token);
|
setToken(result.token);
|
||||||
setGuacamoleConnectionId(result.guacamoleConnectionId ?? null);
|
setGuacamoleConnectionId(result.guacamoleConnectionId ?? null);
|
||||||
logActivity(resolvedProtocolForConnect, hostId, hostName).catch(() => {});
|
if (hostId !== 0) {
|
||||||
|
logActivity(resolvedProtocolForConnect, hostId, hostName).catch(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
hostId,
|
hostId,
|
||||||
@@ -270,7 +333,7 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
protocol,
|
protocol,
|
||||||
promptedCredentials,
|
promptedCredentials,
|
||||||
resolvedProtocolForConnect,
|
resolvedProtocolForConnect,
|
||||||
hostConfig.syncId,
|
hostConfig,
|
||||||
addLog,
|
addLog,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -3686,6 +3686,11 @@
|
|||||||
"privateKeyPlaceholder": "Paste private key...",
|
"privateKeyPlaceholder": "Paste private key...",
|
||||||
"credentialLabel": "Credential",
|
"credentialLabel": "Credential",
|
||||||
"credentialPlaceholder": "Select a saved credential",
|
"credentialPlaceholder": "Select a saved credential",
|
||||||
|
"protocolLabel": "Protocol",
|
||||||
|
"domainLabel": "Domain",
|
||||||
|
"domainPlaceholder": "optional",
|
||||||
|
"connectToRdp": "Connect via RDP",
|
||||||
|
"connectToVnc": "Connect via VNC",
|
||||||
"connectToTerminal": "Connect to Terminal",
|
"connectToTerminal": "Connect to Terminal",
|
||||||
"connectToFiles": "Connect to Files"
|
"connectToFiles": "Connect to Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ import type {
|
|||||||
import type { GuacamoleAppHandle } from "@/features/guacamole/GuacamoleApp";
|
import type { GuacamoleAppHandle } from "@/features/guacamole/GuacamoleApp";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
import type { Tab, TabType, Host } from "@/types/ui-types";
|
import type { Tab, TabType, Host } from "@/types/ui-types";
|
||||||
|
import {
|
||||||
|
isQuickConnectHost,
|
||||||
|
quickConnectGuacHost,
|
||||||
|
} from "@/sidebar/quick-connect-host";
|
||||||
import type { SSHHost } from "@/types";
|
import type { SSHHost } from "@/types";
|
||||||
import { useTabsSafe } from "@/shell/TabContext";
|
import { useTabsSafe } from "@/shell/TabContext";
|
||||||
import {
|
import {
|
||||||
@@ -586,6 +590,9 @@ export function renderTabContent(
|
|||||||
tabId={tab.id}
|
tabId={tab.id}
|
||||||
protocol={tab.type as "rdp" | "vnc" | "telnet"}
|
protocol={tab.type as "rdp" | "vnc" | "telnet"}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
|
quickConnectHost={
|
||||||
|
isQuickConnectHost(host) ? quickConnectGuacHost(host) : undefined
|
||||||
|
}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,38 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Eye, EyeOff, FolderSearch, Terminal } from "lucide-react";
|
import {
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
FolderSearch,
|
||||||
|
Monitor,
|
||||||
|
MousePointerClick,
|
||||||
|
Terminal,
|
||||||
|
} from "lucide-react";
|
||||||
import { Input } from "@/components/input";
|
import { Input } from "@/components/input";
|
||||||
import type { Host } from "@/types/ui-types";
|
import type { Host } from "@/types/ui-types";
|
||||||
import { getCredentials } from "@/api/credentials-api";
|
import { getCredentials } from "@/api/credentials-api";
|
||||||
import { mapCredentials } from "./HostManagerData";
|
import { mapCredentials } from "./HostManagerData";
|
||||||
import { createQuickConnectHost } from "./quick-connect-host";
|
import {
|
||||||
|
createQuickConnectHost,
|
||||||
|
type QuickConnectProtocol,
|
||||||
|
} from "./quick-connect-host";
|
||||||
|
|
||||||
|
const DEFAULT_PORTS: Record<QuickConnectProtocol, string> = {
|
||||||
|
ssh: "22",
|
||||||
|
rdp: "3389",
|
||||||
|
vnc: "5900",
|
||||||
|
};
|
||||||
|
|
||||||
interface QuickConnectPanelProps {
|
interface QuickConnectPanelProps {
|
||||||
onConnect: (host: Host, type: "terminal" | "files") => void;
|
onConnect: (host: Host, type: "terminal" | "files" | "rdp" | "vnc") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [host, setHost] = useState("");
|
const [host, setHost] = useState("");
|
||||||
|
const [protocol, setProtocol] = useState<QuickConnectProtocol>("ssh");
|
||||||
const [port, setPort] = useState("22");
|
const [port, setPort] = useState("22");
|
||||||
|
const [domain, setDomain] = useState("");
|
||||||
const [username, setUsername] = useState("root");
|
const [username, setUsername] = useState("root");
|
||||||
const [authType, setAuthType] = useState<"password" | "key" | "credential">(
|
const [authType, setAuthType] = useState<"password" | "key" | "credential">(
|
||||||
"password",
|
"password",
|
||||||
@@ -33,23 +51,56 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const connect = (type: "terminal" | "files") => {
|
const isDesktop = protocol !== "ssh";
|
||||||
if (!host || !username) return;
|
|
||||||
|
const switchProtocol = (next: QuickConnectProtocol) => {
|
||||||
|
// Keep a port the user typed; only swap the protocol default.
|
||||||
|
if (port === DEFAULT_PORTS[protocol]) setPort(DEFAULT_PORTS[next]);
|
||||||
|
setProtocol(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const connect = (type: "terminal" | "files" | "rdp" | "vnc") => {
|
||||||
|
if (!host) return;
|
||||||
|
if (!isDesktop && !username) return;
|
||||||
const hostConfig = createQuickConnectHost({
|
const hostConfig = createQuickConnectHost({
|
||||||
ip: host,
|
ip: host,
|
||||||
port: parseInt(port) || 22,
|
port: parseInt(port) || parseInt(DEFAULT_PORTS[protocol]),
|
||||||
username,
|
username,
|
||||||
authType,
|
authType: isDesktop ? "password" : authType,
|
||||||
password,
|
password,
|
||||||
key: privateKey,
|
key: privateKey,
|
||||||
credentialId,
|
credentialId,
|
||||||
|
protocol,
|
||||||
|
domain: domain || undefined,
|
||||||
});
|
});
|
||||||
onConnect(hostConfig, type);
|
onConnect(hostConfig, type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const connectDefault = () => connect(isDesktop ? protocol : "terminal");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
|
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
|
||||||
<div className="flex flex-col gap-3 p-3">
|
<div className="flex flex-col gap-3 p-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("newUi.sidebar.quickConnect.protocolLabel")}
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{(["ssh", "rdp", "vnc"] as const).map((type) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
onClick={() => switchProtocol(type)}
|
||||||
|
className={`flex-1 py-1 text-[10px] font-semibold border transition-colors uppercase ${
|
||||||
|
protocol === type
|
||||||
|
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand"
|
||||||
|
: "border-border text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{type}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("newUi.sidebar.quickConnect.hostLabel")}
|
{t("newUi.sidebar.quickConnect.hostLabel")}
|
||||||
@@ -59,7 +110,7 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
|||||||
value={host}
|
value={host}
|
||||||
onChange={(e) => setHost(e.target.value)}
|
onChange={(e) => setHost(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") connect("terminal");
|
if (e.key === "Enter") connectDefault();
|
||||||
}}
|
}}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
@@ -73,7 +124,7 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
|||||||
value={port}
|
value={port}
|
||||||
onChange={(e) => setPort(e.target.value)}
|
onChange={(e) => setPort(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") connect("terminal");
|
if (e.key === "Enter") connectDefault();
|
||||||
}}
|
}}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
@@ -93,11 +144,12 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
|||||||
}}
|
}}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") connect("terminal");
|
if (e.key === "Enter") connectDefault();
|
||||||
}}
|
}}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{!isDesktop && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("newUi.sidebar.quickConnect.authLabel")}
|
{t("newUi.sidebar.quickConnect.authLabel")}
|
||||||
@@ -118,7 +170,8 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{authType === "password" && (
|
)}
|
||||||
|
{(isDesktop || authType === "password") && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("newUi.sidebar.quickConnect.passwordLabel")}
|
{t("newUi.sidebar.quickConnect.passwordLabel")}
|
||||||
@@ -132,7 +185,7 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") connect("terminal");
|
if (e.key === "Enter") connectDefault();
|
||||||
}}
|
}}
|
||||||
className="h-7 text-xs pr-8"
|
className="h-7 text-xs pr-8"
|
||||||
/>
|
/>
|
||||||
@@ -149,7 +202,23 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{authType === "key" && (
|
{isDesktop && protocol === "rdp" && (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||||
|
{t("newUi.sidebar.quickConnect.domainLabel")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("newUi.sidebar.quickConnect.domainPlaceholder")}
|
||||||
|
value={domain}
|
||||||
|
onChange={(e) => setDomain(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") connectDefault();
|
||||||
|
}}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isDesktop && authType === "key" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("newUi.sidebar.quickConnect.privateKeyLabel")}
|
{t("newUi.sidebar.quickConnect.privateKeyLabel")}
|
||||||
@@ -164,7 +233,7 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{authType === "credential" && (
|
{!isDesktop && authType === "credential" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||||
{t("newUi.sidebar.quickConnect.credentialLabel")}
|
{t("newUi.sidebar.quickConnect.credentialLabel")}
|
||||||
@@ -191,6 +260,24 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-1.5 pt-1">
|
<div className="flex flex-col gap-1.5 pt-1">
|
||||||
|
{isDesktop ? (
|
||||||
|
<button
|
||||||
|
onClick={() => connect(protocol)}
|
||||||
|
className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors"
|
||||||
|
>
|
||||||
|
{protocol === "rdp" ? (
|
||||||
|
<Monitor className="size-3.5" />
|
||||||
|
) : (
|
||||||
|
<MousePointerClick className="size-3.5" />
|
||||||
|
)}
|
||||||
|
{t(
|
||||||
|
protocol === "rdp"
|
||||||
|
? "newUi.sidebar.quickConnect.connectToRdp"
|
||||||
|
: "newUi.sidebar.quickConnect.connectToVnc",
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => connect("terminal")}
|
onClick={() => connect("terminal")}
|
||||||
className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors"
|
className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors"
|
||||||
@@ -205,6 +292,8 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
|||||||
<FolderSearch className="size-3.5" />
|
<FolderSearch className="size-3.5" />
|
||||||
{t("newUi.sidebar.quickConnect.connectToFiles")}
|
{t("newUi.sidebar.quickConnect.connectToFiles")}
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,47 @@
|
|||||||
import type { SSHHostData } from "@/types";
|
import type { SSHHostData } from "@/types";
|
||||||
import type { Host } from "@/types/ui-types";
|
import type { Host } from "@/types/ui-types";
|
||||||
|
import type { GuacamoleQuickHost } from "@/features/guacamole/GuacamoleApp";
|
||||||
|
|
||||||
|
export type QuickConnectProtocol = "ssh" | "rdp" | "vnc";
|
||||||
|
|
||||||
type QuickConnectInput = Pick<
|
type QuickConnectInput = Pick<
|
||||||
Host,
|
Host,
|
||||||
"ip" | "port" | "username" | "authType" | "password" | "key" | "credentialId"
|
"ip" | "port" | "username" | "authType" | "password" | "key" | "credentialId"
|
||||||
>;
|
> & { protocol?: QuickConnectProtocol; domain?: string };
|
||||||
|
|
||||||
|
export const QUICK_CONNECT_ID_PREFIX = "quick-connect-";
|
||||||
|
|
||||||
|
export function isQuickConnectHost(host: Pick<Host, "id">): boolean {
|
||||||
|
return host.id.startsWith(QUICK_CONNECT_ID_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
export function createQuickConnectHost(input: QuickConnectInput): Host {
|
export function createQuickConnectHost(input: QuickConnectInput): Host {
|
||||||
|
const protocol = input.protocol ?? "ssh";
|
||||||
|
if (protocol !== "ssh") {
|
||||||
return {
|
return {
|
||||||
id: `quick-connect-${Date.now()}`,
|
...createQuickConnectHost({ ...input, protocol: "ssh", port: 22 }),
|
||||||
|
port: input.port,
|
||||||
|
enableTerminal: false,
|
||||||
|
enableCommandHistory: false,
|
||||||
|
enableFileManager: false,
|
||||||
|
enableTunnel: false,
|
||||||
|
enableDocker: false,
|
||||||
|
enableTerminalToolbar: false,
|
||||||
|
enableSsh: false,
|
||||||
|
enableRdp: protocol === "rdp",
|
||||||
|
enableVnc: protocol === "vnc",
|
||||||
|
rdpPort: protocol === "rdp" ? input.port : 3389,
|
||||||
|
vncPort: protocol === "vnc" ? input.port : 5900,
|
||||||
|
rdpAuthType: "direct",
|
||||||
|
rdpUser: protocol === "rdp" ? input.username : undefined,
|
||||||
|
rdpPassword: protocol === "rdp" ? input.password : undefined,
|
||||||
|
domain: protocol === "rdp" ? input.domain : undefined,
|
||||||
|
vncUser: protocol === "vnc" ? input.username : undefined,
|
||||||
|
vncPassword: protocol === "vnc" ? input.password : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: `${QUICK_CONNECT_ID_PREFIX}${Date.now()}`,
|
||||||
name: `${input.username}@${input.ip}`,
|
name: `${input.username}@${input.ip}`,
|
||||||
ip: input.ip,
|
ip: input.ip,
|
||||||
port: input.port,
|
port: input.port,
|
||||||
@@ -87,3 +120,20 @@ export function quickConnectHostToPayload(host: Host): SSHHostData {
|
|||||||
telnetPort: host.telnetPort,
|
telnetPort: host.telnetPort,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The slice of a quick-connect host that GuacamoleApp mints a token from. */
|
||||||
|
export function quickConnectGuacHost(host: Host): GuacamoleQuickHost {
|
||||||
|
return {
|
||||||
|
name: host.name,
|
||||||
|
ip: host.ip,
|
||||||
|
connectionType: host.enableVnc ? "vnc" : "rdp",
|
||||||
|
domain: host.domain,
|
||||||
|
rdpPort: host.rdpPort,
|
||||||
|
vncPort: host.vncPort,
|
||||||
|
rdpAuthType: host.rdpAuthType,
|
||||||
|
rdpUser: host.rdpUser,
|
||||||
|
rdpPassword: host.rdpPassword,
|
||||||
|
vncUser: host.vncUser,
|
||||||
|
vncPassword: host.vncPassword,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
createQuickConnectHost,
|
createQuickConnectHost,
|
||||||
|
isQuickConnectHost,
|
||||||
|
quickConnectGuacHost,
|
||||||
quickConnectHostToPayload,
|
quickConnectHostToPayload,
|
||||||
} from "../../sidebar/quick-connect-host";
|
} from "../../sidebar/quick-connect-host";
|
||||||
|
|
||||||
@@ -47,3 +49,72 @@ describe("quick connect host", () => {
|
|||||||
expect(payload.key).toBeUndefined();
|
expect(payload.key).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("createQuickConnectHost for remote desktop protocols", () => {
|
||||||
|
it("builds an SSH host by default", () => {
|
||||||
|
const host = createQuickConnectHost({
|
||||||
|
ip: "10.0.0.1",
|
||||||
|
port: 2222,
|
||||||
|
username: "root",
|
||||||
|
authType: "password",
|
||||||
|
password: "pw",
|
||||||
|
});
|
||||||
|
expect(isQuickConnectHost(host)).toBe(true);
|
||||||
|
expect(host).toMatchObject({
|
||||||
|
enableSsh: true,
|
||||||
|
enableRdp: false,
|
||||||
|
sshPort: 2222,
|
||||||
|
password: "pw",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds an RDP host that GuacamoleApp can mint a token from", () => {
|
||||||
|
const host = createQuickConnectHost({
|
||||||
|
ip: "10.0.0.2",
|
||||||
|
port: 3390,
|
||||||
|
username: "admin",
|
||||||
|
authType: "password",
|
||||||
|
password: "pw",
|
||||||
|
protocol: "rdp",
|
||||||
|
domain: "CORP",
|
||||||
|
});
|
||||||
|
expect(host).toMatchObject({
|
||||||
|
enableSsh: false,
|
||||||
|
enableRdp: true,
|
||||||
|
enableVnc: false,
|
||||||
|
rdpPort: 3390,
|
||||||
|
rdpUser: "admin",
|
||||||
|
rdpPassword: "pw",
|
||||||
|
domain: "CORP",
|
||||||
|
});
|
||||||
|
expect(quickConnectGuacHost(host)).toMatchObject({
|
||||||
|
ip: "10.0.0.2",
|
||||||
|
connectionType: "rdp",
|
||||||
|
rdpPort: 3390,
|
||||||
|
rdpUser: "admin",
|
||||||
|
rdpPassword: "pw",
|
||||||
|
domain: "CORP",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds a VNC host with the password on the VNC fields", () => {
|
||||||
|
const host = createQuickConnectHost({
|
||||||
|
ip: "10.0.0.3",
|
||||||
|
port: 5901,
|
||||||
|
username: "",
|
||||||
|
authType: "password",
|
||||||
|
password: "vncpw",
|
||||||
|
protocol: "vnc",
|
||||||
|
});
|
||||||
|
expect(host).toMatchObject({
|
||||||
|
enableVnc: true,
|
||||||
|
vncPort: 5901,
|
||||||
|
vncPassword: "vncpw",
|
||||||
|
});
|
||||||
|
expect(quickConnectGuacHost(host)).toMatchObject({
|
||||||
|
connectionType: "vnc",
|
||||||
|
vncPort: 5901,
|
||||||
|
vncPassword: "vncpw",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user