-
-
-
- {["ssh", ...pluginProtocols.map((entry) => entry.id)].map(
- (type) => (
-
- ),
- )}
-
+
+
+ switchProtocol(e.target.value)}
+ className={SELECT_CLASS}
+ >
+
+ {pluginProtocols.map((entry) => (
+
+ ))}
+
+
+
+ setPort(e.target.value)}
+ onKeyDown={onEnter}
+ className="h-7 text-xs"
+ />
+
-
-
+
setHost(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") connectDefault();
- }}
+ onKeyDown={onEnter}
className="h-7 text-xs"
/>
-
-
-
- setPort(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") connectDefault();
- }}
- className="h-7 text-xs"
- />
-
-
-
+
+
setUsername(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") connectDefault();
- }}
+ onKeyDown={onEnter}
className="h-7 text-xs"
/>
-
- {!isDesktop && (
-
-
-
+
+ {!isDesktop && authOptions.length > 0 && (
+
+ {
+ setAuthType(e.target.value);
+ setAuthFields({});
+ }}
+ className={SELECT_CLASS}
+ >
{authOptions.map((option) => (
-
+
))}
-
-
+
+
)}
{(isDesktop || authType === "password") && (
-
-
+
setPassword(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") connectDefault();
- }}
+ onKeyDown={onEnter}
className="h-7 text-xs pr-8"
/>
-
+
)}
{selected?.quickConnect?.showDomain && (
-
-
+
setDomain(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") connectDefault();
- }}
+ onKeyDown={onEnter}
className="h-7 text-xs"
/>
-
+
)}
{!isDesktop && authType === "key" && (
-
-
+
+
)}
{!isDesktop && authType === "credential" && (
-
-
+
{
@@ -268,7 +252,7 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
const cred = credentials.find((c) => c.id === newId);
if (cred?.username) setUsername(cred.username);
}}
- className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
+ className={SELECT_CLASS}
>
))}
-
+
)}
{!isDesktop && AuthEditor && (
)}
- {selected ? (
-
+ {targets.length === 0 ? (
+
+ {t("newUi.sidebar.quickConnect.noTarget")}
+
) : (
- <>
+ targets.map((action, index) => (
- {quickActions.map((action) => (
-
- ))}
- >
+ ))
)}
diff --git a/src/ui/sidebar/quick-connect-host.ts b/src/ui/sidebar/quick-connect-host.ts
index 6c8911283..7ade420e7 100644
--- a/src/ui/sidebar/quick-connect-host.ts
+++ b/src/ui/sidebar/quick-connect-host.ts
@@ -1,6 +1,7 @@
import type { SSHHostData } from "@/types";
import type { Host } from "@/types/ui-types";
import type { HostProtocolDef } from "./host-protocols";
+import { hostActionsFor, type HostActionDef } from "./host-contributions";
type QuickConnectInput = Pick<
Host,
@@ -98,3 +99,22 @@ export function quickConnectHostToPayload(host: Host): SSHHostData {
sshPort: host.sshPort,
};
}
+
+/**
+ * Every way to reach the address: connect actions (terminal, RDP, VNC...)
+ * by priority, then tools that opted into Quick Connect. Only actions that
+ * open a tab work here, since the host is never saved.
+ */
+export function quickConnectTargets(
+ all: HostActionDef[],
+ host: Host,
+): HostActionDef[] {
+ const usable = hostActionsFor(all, host).filter((action) => action.tabType);
+ const connects = usable
+ .filter((action) => action.kind === "connect")
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
+ const tools = usable.filter(
+ (action) => action.kind !== "connect" && action.quickConnect,
+ );
+ return [...connects, ...tools];
+}
diff --git a/src/ui/tests/sidebar/QuickConnectPanel.test.tsx b/src/ui/tests/sidebar/QuickConnectPanel.test.tsx
new file mode 100644
index 000000000..1bfa84c11
--- /dev/null
+++ b/src/ui/tests/sidebar/QuickConnectPanel.test.tsx
@@ -0,0 +1,131 @@
+import { describe, it, expect, vi, afterEach } from "vitest";
+import {
+ render,
+ screen,
+ cleanup,
+ fireEvent,
+ waitFor,
+} from "@testing-library/react";
+import { FolderSearch, Monitor, Server, Terminal } from "lucide-react";
+
+vi.mock("@/api/credentials-api", () => ({
+ getCredentials: vi.fn(async () => []),
+}));
+vi.mock("@/api/auth-methods-api", () => ({
+ getSshAuthProviders: vi.fn(async () => []),
+}));
+vi.mock("sonner", () => ({ toast: { error: vi.fn() } }));
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ t: (key: string, opts?: Record
) =>
+ opts ? `${key}:${JSON.stringify(opts)}` : key,
+ }),
+}));
+
+import { QuickConnectPanel } from "../../sidebar/QuickConnectPanel";
+import {
+ registerHostAction,
+ resetHostContributions,
+} from "../../sidebar/host-contributions";
+
+afterEach(() => {
+ cleanup();
+ resetHostContributions();
+});
+
+function registerDefaults() {
+ registerHostAction({
+ id: "terminal",
+ titleKey: "nav.terminal",
+ icon: Terminal,
+ kind: "connect",
+ priority: 100,
+ tabType: "terminal",
+ when: (host) => !!host.enableSsh,
+ });
+ registerHostAction({
+ id: "files",
+ titleKey: "nav.files",
+ icon: FolderSearch,
+ kind: "open",
+ tabType: "files",
+ quickConnect: true,
+ when: (host) => !!host.enableSsh,
+ });
+ registerHostAction({
+ id: "docker",
+ titleKey: "nav.docker",
+ icon: Server,
+ kind: "open",
+ tabType: "docker",
+ when: (host) => !!host.enableSsh,
+ });
+ registerHostAction({
+ id: "rdp",
+ titleKey: "hosts.tabRdp",
+ icon: Monitor,
+ kind: "connect",
+ priority: 50,
+ tabType: "rdp",
+ when: () => false,
+ });
+}
+
+const buttonName = (key: string) =>
+ `newUi.sidebar.quickConnect.connectToAction:${JSON.stringify({ name: key })}`;
+
+describe("QuickConnectPanel", () => {
+ it("offers every usable connect target and opt-in tool", () => {
+ registerDefaults();
+ render();
+ expect(screen.getByText(buttonName("nav.terminal"))).toBeTruthy();
+ expect(screen.getByText(buttonName("nav.files"))).toBeTruthy();
+ expect(screen.queryByText(buttonName("nav.docker"))).toBeNull();
+ expect(screen.queryByText(buttonName("hosts.tabRdp"))).toBeNull();
+ });
+
+ it("opens the chosen tab for the typed address", async () => {
+ registerDefaults();
+ const onConnect = vi.fn();
+ render();
+ fireEvent.change(
+ screen.getByPlaceholderText("newUi.sidebar.quickConnect.hostPlaceholder"),
+ { target: { value: "10.0.0.1" } },
+ );
+ fireEvent.click(screen.getByText(buttonName("nav.files")));
+ await waitFor(() => expect(onConnect).toHaveBeenCalledTimes(1));
+ const [host, type] = onConnect.mock.calls[0];
+ expect(type).toBe("files");
+ expect(host).toMatchObject({ ip: "10.0.0.1", username: "root", port: 22 });
+ });
+
+ it("connects with the top target on Enter", () => {
+ registerDefaults();
+ const onConnect = vi.fn();
+ render();
+ const input = screen.getByPlaceholderText(
+ "newUi.sidebar.quickConnect.hostPlaceholder",
+ );
+ fireEvent.change(input, { target: { value: "example.com" } });
+ fireEvent.keyDown(input, { key: "Enter" });
+ expect(onConnect).toHaveBeenCalledWith(
+ expect.objectContaining({ ip: "example.com" }),
+ "terminal",
+ );
+ });
+
+ it("does nothing without an address", () => {
+ registerDefaults();
+ const onConnect = vi.fn();
+ render();
+ fireEvent.click(screen.getByText(buttonName("nav.terminal")));
+ expect(onConnect).not.toHaveBeenCalled();
+ });
+
+ it("says so when nothing can connect", () => {
+ render();
+ expect(
+ screen.getByText("newUi.sidebar.quickConnect.noTarget"),
+ ).toBeTruthy();
+ });
+});
diff --git a/src/ui/tests/sidebar/quick-connect-host.test.ts b/src/ui/tests/sidebar/quick-connect-host.test.ts
index 5e79bbf07..e807603a8 100644
--- a/src/ui/tests/sidebar/quick-connect-host.test.ts
+++ b/src/ui/tests/sidebar/quick-connect-host.test.ts
@@ -3,7 +3,9 @@ import {
createQuickConnectHost,
isQuickConnectHost,
quickConnectHostToPayload,
+ quickConnectTargets,
} from "../../sidebar/quick-connect-host";
+import type { HostActionDef } from "../../sidebar/host-contributions";
describe("quick connect host", () => {
afterEach(() => vi.restoreAllMocks());
@@ -132,3 +134,44 @@ describe("createQuickConnectHost for remote desktop protocols", () => {
).toBe(false);
});
});
+
+describe("quickConnectTargets", () => {
+ const action = (over: Partial): HostActionDef => ({
+ id: "x",
+ titleKey: "x",
+ icon: () => null,
+ kind: "open",
+ when: () => true,
+ ...over,
+ });
+ const host = createQuickConnectHost({
+ ip: "10.0.0.3",
+ port: 22,
+ username: "root",
+ authType: "password",
+ });
+
+ it("puts connect actions first by priority, then opted-in tools", () => {
+ const targets = quickConnectTargets(
+ [
+ action({ id: "files", tabType: "files", quickConnect: true }),
+ action({ id: "docker", tabType: "docker" }),
+ action({ id: "vnc", kind: "connect", priority: 40, tabType: "vnc" }),
+ action({ id: "term", kind: "connect", priority: 100, tabType: "t" }),
+ action({ id: "run-only", kind: "connect", priority: 200 }),
+ action({
+ id: "off",
+ kind: "connect",
+ tabType: "off",
+ when: () => false,
+ }),
+ ],
+ host,
+ );
+ expect(targets.map((target) => target.id)).toEqual([
+ "term",
+ "vnc",
+ "files",
+ ]);
+ });
+});