mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
feat(remote): Show the stages and transfer progress of connecting; primary button hover follows the accent
This commit is contained in:
1 parent
6bcf99778a
commit
ec41d3c4a0
10 files changed
+168
-21
No files matched your search
+9
-1
@@ -854,7 +854,7 @@ export const commands = {
|
||||
* File › Remote Development: opens `path` on the host of SSH configuration
|
||||
* `ssh_config` in this window (installs / starts the server as needed).
|
||||
*/
|
||||
remoteConnect: (sshConfig: string, path: string) => typedError<RemoteProject_Serialize, IdeError>(__TAURI_INVOKE("remote_connect", { sshConfig, path })),
|
||||
remoteConnect: (sshConfig: string, path: string, onProgress: Channel<RemoteProgress>) => typedError<RemoteProject_Serialize, IdeError>(__TAURI_INVOKE("remote_connect", { sshConfig, path, onProgress })),
|
||||
/** The remote session of this window (a settings window: its parent's), if any. */
|
||||
remoteCurrent: () => __TAURI_INVOKE<{
|
||||
sshConfig: string,
|
||||
@@ -4412,6 +4412,11 @@ tags?: PushTags | null } |
|
||||
/** Pull dialog: `branches` of `remote` (none = the configured merge branch). */
|
||||
{ op: "pullFrom"; remote: string; branches: string[]; rebase: boolean; options: MergeOptions };
|
||||
|
||||
export type RemoteProgress = {
|
||||
stage: RemoteStage,
|
||||
fraction: number | null,
|
||||
};
|
||||
|
||||
export type RemoteProject = RemoteProject_Serialize | RemoteProject_Deserialize;
|
||||
|
||||
export type RemoteProject_Deserialize = {
|
||||
@@ -4445,6 +4450,9 @@ export type RemoteSession = {
|
||||
commands: string[],
|
||||
};
|
||||
|
||||
/** Where [`remote_connect`] is (`fraction`: 0..1 of a download / upload). */
|
||||
export type RemoteStage = "connecting" | "checking" | "downloading" | "uploading" | "starting" | "opening";
|
||||
|
||||
export type ReplaceEvent =
|
||||
/** A file was changed (`replacements` > 0) or had nothing left to replace. */
|
||||
{ type: "file"; path: IpcPath; replacements: number; done: number; total: number } | { type: "failed"; path: IpcPath; message: string; done: number; total: number } | { type: "done"; summary: ReplaceSummary };
|
||||
|
||||
@@ -3,11 +3,18 @@ import { useState } from "react";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { useTx } from "@/platform/i18n";
|
||||
import { call, commands, type RemoteEntry } from "@/platform/ipc";
|
||||
import { connectRemote } from "@/platform/remote";
|
||||
import { connectRemote, useRemoteProgress } from "@/platform/remote";
|
||||
import { sshLabel } from "@/platform/remoteSettings";
|
||||
import { useWorkflowSettings } from "@/platform/workflowSettings";
|
||||
import { Button, Dialog, Icon, Input, Select } from "@/ui";
|
||||
import { browsableFolders, closeConnectDialog, listedDirectory, remoteParent, useConnectDialog } from "./model";
|
||||
import { Button, Dialog, Icon, Input, ProgressBar, Select, Spinner } from "@/ui";
|
||||
import {
|
||||
browsableFolders,
|
||||
closeConnectDialog,
|
||||
listedDirectory,
|
||||
remoteParent,
|
||||
stageLabel,
|
||||
useConnectDialog,
|
||||
} from "./model";
|
||||
import styles from "./RemoteDev.module.css";
|
||||
|
||||
export function ConnectHost() {
|
||||
@@ -130,6 +137,7 @@ function ConnectDialog() {
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<ConnectProgress />
|
||||
{error && (
|
||||
<div className={styles.error} role="alert">
|
||||
{error}
|
||||
@@ -139,3 +147,21 @@ function ConnectDialog() {
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/** The stage of the connection in progress (and how far a download / upload is). */
|
||||
function ConnectProgress() {
|
||||
const tx = useTx();
|
||||
const progress = useRemoteProgress((s) => s.progress);
|
||||
if (!progress) return null;
|
||||
const percent = progress.fraction == null ? null : Math.round(progress.fraction * 100);
|
||||
return (
|
||||
<div className={styles.progress} role="status">
|
||||
<div className={styles.progressLine}>
|
||||
<Spinner size={12} />
|
||||
<span>{tx(stageLabel(progress.stage))}</span>
|
||||
{percent != null && <span className={styles.percent}>{percent}%</span>}
|
||||
</div>
|
||||
<ProgressBar value={progress.fraction} aria-label={tx(stageLabel(progress.stage))} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -53,3 +53,22 @@
|
||||
overflow-wrap: anywhere;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.progressLine {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
|
||||
.percent {
|
||||
margin-left: auto;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/** File › Remote Development…: state of the connect dialog and the folder browser helpers. */
|
||||
import { create } from "zustand";
|
||||
import type { RemoteEntry } from "@/platform/ipc";
|
||||
import type { RemoteEntry, RemoteStage } from "@/platform/ipc";
|
||||
|
||||
export const useConnectDialog = create<{ open: boolean }>(() => ({ open: false }));
|
||||
|
||||
@@ -33,3 +33,21 @@ export function listedDirectory(requested: string, entries: readonly RemoteEntry
|
||||
const first = entries[0];
|
||||
return first ? remoteParent(first.path) : requested;
|
||||
}
|
||||
|
||||
/** What the window is waiting for at each stage of connecting. */
|
||||
export function stageLabel(stage: RemoteStage): string {
|
||||
switch (stage) {
|
||||
case "connecting":
|
||||
return "Connecting to the host…";
|
||||
case "checking":
|
||||
return "Checking the IDE backend on the host…";
|
||||
case "downloading":
|
||||
return "Downloading the IDE backend…";
|
||||
case "uploading":
|
||||
return "Installing the IDE backend on the host…";
|
||||
case "starting":
|
||||
return "Starting the IDE backend…";
|
||||
case "opening":
|
||||
return "Opening the project…";
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import clsx from "clsx";
|
||||
import { executeAction, useActions } from "@/platform/actions";
|
||||
import { shortcutLabel } from "@/platform/keymap";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useRemoteProgress } from "@/platform/remote";
|
||||
import { stageLabel } from "@/features/remote-dev/model";
|
||||
import {
|
||||
Button,
|
||||
HighlightedText,
|
||||
@@ -108,6 +110,11 @@ function ProjectRow({
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const opening = useRecent((s) => s.opening === m.project.path);
|
||||
// Opening a remote project: which stage of connecting it is at.
|
||||
const progress = useRemoteProgress((s) => (opening ? s.progress : null));
|
||||
const stage = progress
|
||||
? tx(stageLabel(progress.stage)) + (progress.fraction == null ? "" : ` ${Math.round(progress.fraction * 100)}%`)
|
||||
: null;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (selected) ref.current?.scrollIntoView?.({ block: "nearest" });
|
||||
@@ -134,7 +141,13 @@ function ProjectRow({
|
||||
<ProjectBadge name={m.project.name} className={styles.projectBadge} />
|
||||
<span className={styles.projectText}>
|
||||
<HighlightedText className={styles.projectName} text={m.project.name} indices={m.nameMatch} tone="strong" />
|
||||
<HighlightedText className={styles.projectPath} text={m.displayPath} indices={m.pathMatch} />
|
||||
{stage ? (
|
||||
<span className={styles.projectPath} role="status">
|
||||
{stage}
|
||||
</span>
|
||||
) : (
|
||||
<HighlightedText className={styles.projectPath} text={m.displayPath} indices={m.pathMatch} />
|
||||
)}
|
||||
{m.project.branch && (
|
||||
<span className={styles.projectBranch}>
|
||||
<Icon name="branch" size={14} />
|
||||
|
||||
@@ -4153,5 +4153,11 @@
|
||||
"Remote Development…": "远程开发…",
|
||||
"Opens a project folder on an SSH host in this window; the IDE backend runs on that host.": "在此窗口中打开 SSH 主机上的项目文件夹;IDE 后端在该主机上运行。",
|
||||
"Disconnect from Remote Host": "断开与远程主机的连接",
|
||||
"Opens a terminal tab connected to an SSH configuration (`arg`: its id; otherwise a chooser).": "打开连接到 SSH 配置的终端标签页(`arg`:配置 id;否则显示选择器)。"
|
||||
"Opens a terminal tab connected to an SSH configuration (`arg`: its id; otherwise a chooser).": "打开连接到 SSH 配置的终端标签页(`arg`:配置 id;否则显示选择器)。",
|
||||
"Connecting to the host…": "正在连接主机…",
|
||||
"Checking the IDE backend on the host…": "正在检查主机上的 IDE 后端…",
|
||||
"Downloading the IDE backend…": "正在下载 IDE 后端…",
|
||||
"Installing the IDE backend on the host…": "正在主机上安装 IDE 后端…",
|
||||
"Starting the IDE backend…": "正在启动 IDE 后端…",
|
||||
"Opening the project…": "正在打开项目…"
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Channel, type InvokeArgs, type InvokeOptions } from "@tauri-apps/api/core";
|
||||
import { invokeLocal, setInvokeRoute, type InvokeRoute } from "./tauriCore";
|
||||
import { create } from "zustand";
|
||||
import { call, commands, isTauri, type ChannelFrame, type RemoteSession } from "./ipc";
|
||||
import { call, commands, isTauri, type ChannelFrame, type RemoteProgress, type RemoteSession } from "./ipc";
|
||||
import { useProject } from "./project";
|
||||
|
||||
interface RemoteState {
|
||||
@@ -96,11 +96,17 @@ export async function restoreRemote(): Promise<RemoteSession | null> {
|
||||
return session;
|
||||
}
|
||||
|
||||
/** Where the connection in progress is (`null`: none). */
|
||||
export const useRemoteProgress = create<{ progress: RemoteProgress | null }>(() => ({ progress: null }));
|
||||
|
||||
/** Opens `path` on the host of SSH configuration `sshConfig` in this window. */
|
||||
export async function connectRemote(sshConfig: string, path: string) {
|
||||
useProject.setState({ opening: true });
|
||||
const onProgress = new Channel<RemoteProgress>();
|
||||
onProgress.onmessage = (progress) => useRemoteProgress.setState({ progress });
|
||||
useRemoteProgress.setState({ progress: { stage: "connecting", fraction: null } });
|
||||
try {
|
||||
const r = await call(commands.remoteConnect(sshConfig, path));
|
||||
const r = await call(commands.remoteConnect(sshConfig, path, onProgress));
|
||||
enableRemote({ sshConfig: r.sshConfig, root: r.root, host: r.host, commands: r.commands });
|
||||
useProject.setState({ project: r.project, settings: r.project.settings });
|
||||
rememberRemote({
|
||||
@@ -113,6 +119,7 @@ export async function connectRemote(sshConfig: string, path: string) {
|
||||
return r;
|
||||
} finally {
|
||||
useProject.setState({ opening: false });
|
||||
useRemoteProgress.setState({ progress: null });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -285,7 +285,7 @@ html {
|
||||
--bg-toolbar-selected: #2e4d89;
|
||||
--bg-toolbar-selected-hover: #2f5eb9;
|
||||
--bg-button-primary: var(--accent);
|
||||
--bg-button-primary-hover: #2f5eb9;
|
||||
--bg-button-primary-hover: var(--accent-hover);
|
||||
--fg-button-primary: #ffffff;
|
||||
--bg-button: transparent;
|
||||
--border-button: #40434a;
|
||||
@@ -569,7 +569,7 @@ html {
|
||||
--bg-toolbar-pressed: #00000020;
|
||||
--bg-toolbar-selected: #d0dffe;
|
||||
--bg-toolbar-selected-hover: #bdd3ff;
|
||||
--bg-button-primary-hover: #2f5eb9;
|
||||
--bg-button-primary-hover: var(--accent-hover);
|
||||
--fg-button-primary: #ffffff;
|
||||
--bg-button: #ffffff;
|
||||
--border-button: #d1d3d9;
|
||||
@@ -783,7 +783,7 @@ html {
|
||||
--bg-toolbar-pressed: #ffffff38;
|
||||
--bg-toolbar-selected: #2a4b8f;
|
||||
--bg-toolbar-selected-hover: #3560b0;
|
||||
--bg-button-primary-hover: #a7c5ff;
|
||||
--bg-button-primary-hover: var(--accent-hover);
|
||||
--fg-button-primary: #000000;
|
||||
--bg-button: transparent;
|
||||
--border-button: #8b8e95;
|
||||
|
||||
Reference in new issue
Block a user