mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
fix: rdp/vnc dupliocated icon, mobile hover fix for menu, and guacd hosts not working when ssh is enabeld
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user