diff --git a/src/ui/sidebar/tree/HostItem/HostItem.tsx b/src/ui/sidebar/tree/HostItem/HostItem.tsx
index d1ce58b2..3e475687 100644
--- a/src/ui/sidebar/tree/HostItem/HostItem.tsx
+++ b/src/ui/sidebar/tree/HostItem/HostItem.tsx
@@ -331,6 +331,10 @@ export function HostItem({
const [authOverrideOpen, setAuthOverrideOpen] = useState(false);
const [parentDragOver, setParentDragOver] = useState(false);
const [nativeRdpAvailable, setNativeRdpAvailable] = useState(false);
+ const [contextMenuPosition, setContextMenuPosition] = useState<{
+ x: number;
+ y: number;
+ } | null>(null);
useEffect(() => {
if (!window.electronAPI?.isElectron) return;
@@ -398,6 +402,16 @@ export function HostItem({
}
}
+ async function handleWakeOnLan(e: MouseEvent) {
+ e.stopPropagation();
+ try {
+ await wakeOnLan(Number(host.id));
+ toast.success(t("hosts.wakeOnLanSuccess", { name: host.name }));
+ } catch {
+ toast.error(t("hosts.wakeOnLanError"));
+ }
+ }
+
if (query && !hostMatchesQuery(host, query)) return null;
const depthStyle =
@@ -504,15 +518,7 @@ export function HostItem({
{host.macAddress && (
)}
-
+ {
+ if (!open) setContextMenuPosition(null);
+ onMenuOpenChange?.(open);
+ }}
+ >
@@ -596,6 +619,100 @@ export function HostItem({
align="start"
className="text-xs w-auto min-w-44 max-w-72 whitespace-nowrap"
>
+
+
+
+ {t("common.connect")}
+
+
+ {sshActions.map(({ type, icon: Icon, label }) => (
+ {
+ e.stopPropagation();
+ openHostTab(type);
+ }}
+ >
+
+ {label}
+
+ ))}
+ {host.enableRdp && (
+ {
+ e.stopPropagation();
+ openHostTab("rdp");
+ }}
+ >
+
+ {t("hosts.connectRdp")}
+
+ )}
+ {host.enableVnc && (
+ {
+ e.stopPropagation();
+ openHostTab("vnc");
+ }}
+ >
+
+ {t("hosts.connectVnc")}
+
+ )}
+ {host.enableTelnet && (
+ {
+ e.stopPropagation();
+ openHostTab("telnet");
+ }}
+ >
+
+ {t("hosts.connectTelnet")}
+
+ )}
+
+
+
+ {onEditHost && (
+ {
+ e.stopPropagation();
+ onEditHost();
+ }}
+ >
+
+ {t("hosts.editHostAction")}
+
+ )}
+ {onShareHost && (
+ {
+ e.stopPropagation();
+ onShareHost();
+ }}
+ >
+
+ {t("hosts.shareHost")}
+
+ )}
+ {host.enableProxmox && onProxmoxDiscover && (
+ {
+ e.stopPropagation();
+ onProxmoxDiscover();
+ }}
+ >
+
+ {t("hosts.proxmoxDiscoverAction")}
+
+ )}
+ {host.macAddress && (
+
+
+ {t("hosts.wakeOnLanAction")}
+
+ )}
+
{
e.stopPropagation();
@@ -929,6 +1046,13 @@ export function HostItem({
}}
onMouseEnter={() => onHoverChange?.(true)}
onMouseLeave={() => onHoverChange?.(false)}
+ onContextMenu={(event) => {
+ if (selectionMode || arrangeMode) return;
+ event.preventDefault();
+ event.stopPropagation();
+ setContextMenuPosition({ x: event.clientX, y: event.clientY });
+ onMenuOpenChange?.(true);
+ }}
className={`group relative flex items-stretch select-none transition-colors hover:bg-muted/50 ${
canDrag ? "cursor-grab active:cursor-grabbing" : "cursor-pointer"
} ${
diff --git a/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx b/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx
index 47d56f86..a63e40af 100644
--- a/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx
+++ b/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx
@@ -1,4 +1,5 @@
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { useState } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { Host } from "@/types/ui-types";
import { LOCAL_ADAPTIVE_PREFERENCES_KEY } from "@/lib/local-adaptive-preferences";
@@ -106,10 +107,41 @@ describe("HostItem density parity", () => {
"exposes the Copy Link submenu trigger in %s density",
(density) => {
renderHostItem(density, { menuOpen: true });
+ expect(screen.getByText("common.connect")).toBeTruthy();
expect(screen.getByText("hosts.copyLink")).toBeTruthy();
},
);
+ it("opens the complete host menu on right click", () => {
+ function Harness() {
+ const [menuOpen, setMenuOpen] = useState(false);
+ return (
+
+ );
+ }
+ render();
+
+ const hostRow = screen.getByText("web-01").closest(".cursor-pointer");
+ expect(hostRow).toBeTruthy();
+ fireEvent.contextMenu(hostRow!, { clientX: 120, clientY: 80 });
+
+ expect(screen.getByText("common.connect")).toBeTruthy();
+ expect(screen.getAllByText("hosts.editHostAction").length).toBeGreaterThan(
+ 0,
+ );
+ expect(screen.getAllByText("hosts.shareHost").length).toBeGreaterThan(0);
+ expect(screen.getByText("hosts.wakeOnLanAction")).toBeTruthy();
+ });
+
it.each(["comfortable", "compact"] as const)(
"exposes edit, share, and more-options actions in %s density",
(density) => {