дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(remote-dev): list this machine's SSH configurations in a remote project's window (#1097)
A remote project's window gets the remote host's settings, which don't include this machine's SSH configurations, so the Remote Development dialog showed "No SSH configurations" there. The connection is made from this machine, so the dialog reads the local global SSH configurations and reloads them when settings change. Other SSH-configuration consumers run on the host and keep the host's settings.
Цей коміт міститься в:
1 джерело
6b9ab3b323
коміт
3a98d5c5be
2 files changed
+56
-24
No files matched your search
@@ -1,20 +1,21 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import type { RemoteEntry, Settings } from "@/platform/ipc";
|
||||
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { emitLocalIdeEvent, type IdeEvent, type RemoteEntry, type Settings } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useRemoteProgress } from "@/platform/remote";
|
||||
import { ConnectHost } from "./ConnectDialog";
|
||||
import { closeConnectDialog, openConnectDialog, useConnectDialog } from "./model";
|
||||
|
||||
const { list, openRemote, executeAction } = vi.hoisted(() => ({
|
||||
const { list, globalSettings, openRemote, executeAction } = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
globalSettings: vi.fn(),
|
||||
openRemote: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("@/platform/ipc")>();
|
||||
return { ...real, commands: { ...real.commands, remoteBootstrapList: list } };
|
||||
return { ...real, commands: { ...real.commands, remoteBootstrapList: list, settingsGetGlobal: globalSettings } };
|
||||
});
|
||||
vi.mock("@/workbench/projectWindow", () => ({ openRemoteProject: openRemote }));
|
||||
vi.mock("@/platform/actions", async (importOriginal) => ({
|
||||
@@ -34,18 +35,16 @@ const entry = (path: string, isDir = true): RemoteEntry => ({
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
useProject.setState({
|
||||
project: null,
|
||||
opening: false,
|
||||
settings: {
|
||||
ssh: {
|
||||
configs: [
|
||||
{ id: "first", name: "First host", host: "first.example", user: "dev" },
|
||||
{ id: "selected", name: "Selected host", host: "selected.example", user: "dev" },
|
||||
],
|
||||
},
|
||||
} as Settings,
|
||||
});
|
||||
// A remote project's window: its settings are the remote host's, without this machine's SSH configurations.
|
||||
useProject.setState({ project: null, opening: false, settings: { ssh: { configs: [] } } as unknown as Settings });
|
||||
globalSettings.mockResolvedValue({
|
||||
ssh: {
|
||||
configs: [
|
||||
{ id: "first", name: "First host", host: "first.example", user: "dev" },
|
||||
{ id: "selected", name: "Selected host", host: "selected.example", user: "dev" },
|
||||
],
|
||||
},
|
||||
} as Settings);
|
||||
useRemoteProgress.setState({ progress: null, location: null });
|
||||
openConnectDialog();
|
||||
});
|
||||
@@ -57,6 +56,7 @@ it("browses the selected SSH host, navigates folders and opens one without a cur
|
||||
list.mockImplementationOnce(() => new Promise((resolve) => (finish = resolve)));
|
||||
openRemote.mockResolvedValue(true);
|
||||
render(<ConnectHost />);
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Browse" })).toBeEnabled());
|
||||
fireEvent.click(screen.getByRole("button", { name: "SSH configuration" }));
|
||||
fireEvent.click(screen.getByRole("option", { name: /Selected host/ }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Browse" }));
|
||||
@@ -95,6 +95,7 @@ it("browses the selected SSH host, navigates folders and opens one without a cur
|
||||
it("shows a connection error and lets the welcome window retry Browse", async () => {
|
||||
list.mockRejectedValueOnce(new Error("Connection refused"));
|
||||
render(<ConnectHost />);
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Browse" })).toBeEnabled());
|
||||
fireEvent.change(screen.getByLabelText("Project directory:"), { target: { value: " /srv/projects " } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Browse" }));
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent("Connection refused");
|
||||
@@ -107,12 +108,20 @@ it("shows a connection error and lets the welcome window retry Browse", async ()
|
||||
expect(openRemote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("offers SSH configuration from the welcome window and disables Browse until one exists", () => {
|
||||
useProject.setState({ settings: { ssh: { configs: [] } } as unknown as Settings });
|
||||
it("offers SSH configuration and disables Browse until one exists, then lists the new one", async () => {
|
||||
globalSettings.mockResolvedValue({ ssh: { configs: [] } } as unknown as Settings);
|
||||
render(<ConnectHost />);
|
||||
await waitFor(() => expect(globalSettings).toHaveBeenCalled());
|
||||
expect(screen.getByRole("button", { name: "Browse" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Open" })).toBeDisabled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Configure…" }));
|
||||
expect(executeAction).toHaveBeenCalledWith("file.settings", "tools.sshConfigurations");
|
||||
expect(list).not.toHaveBeenCalled();
|
||||
|
||||
globalSettings.mockResolvedValue({
|
||||
ssh: { configs: [{ id: "dev", name: "dev", host: "192.168.1.2", user: "robin" }] },
|
||||
} as Settings);
|
||||
act(() => emitLocalIdeEvent({ type: "settingsChanged", key: "ssh.configs" } as IdeEvent));
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Browse" })).toBeEnabled());
|
||||
expect(screen.getByRole("button", { name: "SSH configuration" })).toHaveTextContent("dev");
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
/** Picks an SSH configuration and a folder on that host, then opens it in this or a new window. */
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { useTx } from "@/platform/i18n";
|
||||
import { call, commands, isCancelled, type RemoteEntry } from "@/platform/ipc";
|
||||
import { call, commands, isCancelled, onIdeEvent, type RemoteEntry } from "@/platform/ipc";
|
||||
import { useRemoteProgress } from "@/platform/remote";
|
||||
import { sshLabel } from "@/platform/remoteSettings";
|
||||
import { useWorkflowSettings } from "@/platform/workflowSettings";
|
||||
import { sshLabel, type SshConfigValid, validateRemote } from "@/platform/remoteSettings";
|
||||
import { Button, Dialog, Icon, Input, ProgressBar, Select, Spinner } from "@/ui";
|
||||
import { openRemoteProject } from "@/workbench/projectWindow";
|
||||
import {
|
||||
@@ -25,10 +24,34 @@ export function ConnectHost() {
|
||||
|
||||
const SSH_SETTINGS_PAGE = "tools.sshConfigurations";
|
||||
|
||||
/**
|
||||
* This machine's SSH configurations: the connection is made from here. A remote project's
|
||||
* window has the remote host's settings, which don't list them.
|
||||
*/
|
||||
function useLocalSshConfigs(): SshConfigValid[] {
|
||||
const [configs, setConfigs] = useState<SshConfigValid[]>([]);
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
const load = () =>
|
||||
void commands.settingsGetGlobal().then(
|
||||
(s) => live && setConfigs(validateRemote(s).ssh.configs),
|
||||
() => {},
|
||||
);
|
||||
load();
|
||||
const off = onIdeEvent("settingsChanged", load);
|
||||
return () => {
|
||||
live = false;
|
||||
off();
|
||||
};
|
||||
}, []);
|
||||
return configs;
|
||||
}
|
||||
|
||||
function ConnectDialog() {
|
||||
const tx = useTx();
|
||||
const configs = useWorkflowSettings((s) => s.ssh.configs);
|
||||
const [ssh, setSsh] = useState(configs[0]?.id ?? "");
|
||||
const configs = useLocalSshConfigs();
|
||||
const [picked, setSsh] = useState("");
|
||||
const ssh = configs.some((c) => c.id === picked) ? picked : (configs[0]?.id ?? "");
|
||||
const [path, setPath] = useState("~");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
Reference in new issue
Заблокувати користувача