主机名称留空时默认使用连接地址

This commit is contained in:
Nex-Z authored and since2006 committed 2026-10-13 00:01:21 +08:00
1 parent 0dfa3fdf77
commit 78fd216fb6
9 files changed
+153 -29

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ ShellRS 同一时间只运行一个:已经开着时再打开一次,会把已
## 主机列表
左侧栏里保存的每一项是一台主机:地址、端口和认证方式。同一台机器用不同用户登录,就保存成两台主机。
左侧栏里保存的每一项是一台主机:地址、端口和认证方式。同一台机器用不同用户登录,就保存成两台主机。名称可以留空,保存时默认使用地址;也可以填写自定义名称。
主机对话框的「认证方式」有三种:
+2 -2
View File
@@ -127,8 +127,8 @@ host.dialog.name:
zh-CN: 名称
en: Name
host.dialog.name_placeholder:
zh-CN: 例如 web-01
en: e.g. web-01
zh-CN: 可选,默认用地址
en: Optional; the address if left empty
host.dialog.temporary_name_placeholder:
zh-CN: 可选,默认用地址
en: Optional; the address if left empty
+26 -2
View File
@@ -740,7 +740,7 @@ mod tests {
}
#[test]
fn a_new_host_needs_a_name_and_an_address_and_takes_the_forms_defaults() {
fn a_new_host_needs_an_address_and_takes_the_forms_defaults() {
let (store, _, _) = store();
let plan = plan_host(
fields(serde_json::json!({ "name": " db ", "host": "10.0.0.2" })),
@@ -761,7 +761,11 @@ mod tests {
);
for (json, message) in [
(serde_json::json!({ "host": "10.0.0.2" }), "请输入名称"),
(serde_json::json!({}), "请输入地址"),
(
serde_json::json!({ "name": "", "host": " " }),
"请输入地址",
),
(serde_json::json!({ "name": "db" }), "请输入地址"),
(
serde_json::json!({ "name": "db", "host": "h", "port": 0 }),
@@ -793,6 +797,26 @@ mod tests {
}
}
#[test]
fn a_host_with_no_name_uses_its_address() {
let (store, web, _) = store();
for json in [
serde_json::json!({ "host": " 10.0.0.2 " }),
serde_json::json!({ "name": "", "host": " 10.0.0.2 " }),
serde_json::json!({ "name": " ", "host": " 10.0.0.2 " }),
] {
let plan = plan_host(fields(json), None, &store).unwrap();
assert_eq!(plan.draft.name.as_ref(), "10.0.0.2");
}
let plan = plan_host(
fields(serde_json::json!({ "name": " " })),
Some(web),
&store,
)
.unwrap();
assert_eq!(plan.draft.name, plan.draft.address);
}
#[test]
fn a_group_path_names_a_group_or_the_groups_to_make() {
let (mut store, web, _) = store();
+9 -11
View File
@@ -677,17 +677,13 @@ impl HostForm {
/// Validate and write to the store. Returns whether the dialog may close.
pub fn commit(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
let name = self.name.read(cx).value().trim().to_string();
let checked = if name.is_empty() {
Err(HostDraftError::Name.message())
} else {
self.endpoint(cx).and_then(|endpoint| {
Ok((
endpoint,
self.committed_login(cx)?,
self.committed_route(cx)?,
))
})
};
let checked = self.endpoint(cx).and_then(|endpoint| {
Ok((
endpoint,
self.committed_login(cx)?,
self.committed_route(cx)?,
))
});
let ((host, port), login, route) = match checked {
Ok(checked) => checked,
Err(error) => {
@@ -696,6 +692,8 @@ impl HostForm {
}
};
let name = if name.is_empty() { host.clone() } else { name };
let group = self
.group
.read(cx)
+39 -5
View File
@@ -585,20 +585,21 @@ impl HostDraft {
SecretRef::password(self.user.as_ref(), self.address.as_ref(), self.port)
}
/// The draft with surrounding blanks removed and an empty user name
/// taken as [`super::DEFAULT_USER`], or the first thing wrong with it,
/// The draft with surrounding blanks removed, an empty name taken as
/// the address and an empty user name as [`super::DEFAULT_USER`], or the
/// first thing wrong with it,
/// in the order the host form checks. What the form cannot leave out
/// of a draft (a credential to pick, a proxy password without a user)
/// is the caller's to check.
pub fn validated(mut self) -> Result<Self, HostDraftError> {
self.name = self.name.trim().to_string().into();
if self.name.is_empty() {
return Err(HostDraftError::Name);
}
self.address = self.address.trim().to_string().into();
if self.address.is_empty() {
return Err(HostDraftError::Address);
}
if self.name.is_empty() {
self.name = self.address.clone();
}
if self.port == 0 {
return Err(HostDraftError::Port);
}
@@ -671,6 +672,39 @@ impl std::error::Error for HostDraftError {}
mod tests {
use super::*;
#[test]
fn a_blank_host_name_uses_the_trimmed_address() {
for (name, expected) in [
("", "example.com"),
(" \t ", "example.com"),
(" web-01 ", "web-01"),
] {
let draft = HostDraft::new(name, " example.com ", 2222, "", AuthKind::Password, None)
.validated()
.unwrap();
assert_eq!(draft.name.as_ref(), expected);
assert_eq!(draft.address.as_ref(), "example.com");
assert_eq!(draft.user.as_ref(), super::super::DEFAULT_USER);
assert_eq!(draft.port, 2222);
}
}
#[test]
fn a_host_still_requires_an_address_with_or_without_a_name() {
for name in ["", " ", "web-01"] {
for address in ["", " \t "] {
let error = HostDraft::new(name, address, 22, "root", AuthKind::Password, None)
.validated()
.unwrap_err();
assert_eq!(error, HostDraftError::Address);
}
}
let error = HostDraft::new("", "example.com", 0, "root", AuthKind::Password, None)
.validated()
.unwrap_err();
assert_eq!(error, HostDraftError::Port);
}
#[test]
fn the_distributions_are_linux_and_the_rest_are_not() {
let not_linux: Vec<_> = HostOs::ALL
+13 -2
View File
@@ -98,10 +98,21 @@ fn a_host_from_the_cli_lands_in_the_tree_with_its_group_and_password(cx: &mut Te
host: Some("10.0.9.2".into()),
..HostFields::default()
};
let error = apply(cx, handle, &workspace, CliChange::CreateHost(nameless)).unwrap_err();
let Ok(Reply::Host(created)) = apply(cx, handle, &workspace, CliChange::CreateHost(nameless))
else {
panic!("host without a name not created");
};
assert_eq!(created.name, "10.0.9.2");
let error = apply(
cx,
handle,
&workspace,
CliChange::CreateHost(HostFields::default()),
)
.unwrap_err();
assert_eq!(
(error.code, error.message.as_str()),
(ErrorCode::BadRequest, "请输入名称")
(ErrorCode::BadRequest, "请输入地址")
);
}
+2 -4
View File
@@ -234,8 +234,6 @@ async fn a_host_can_use_a_credential_instead_of_typing_a_login(cx: &mut TestAppC
);
in_frame(cx, handle, |window, cx| window.click("new-host-panel", cx));
in_frame(cx, handle, |window, cx| {
window.click("host-name", cx);
window.input("web-01", cx);
window.click("host-address", cx);
window.input("10.0.1.12", cx);
window.within("host-auth-source").click(1usize, cx);
@@ -274,8 +272,8 @@ async fn a_host_can_use_a_credential_instead_of_typing_a_login(cx: &mut TestAppC
let created = store
.hosts()
.iter()
.find(|host| host.name == "web-01")
.expect("web-01 inserted");
.find(|host| host.name == "10.0.1.12")
.expect("host named after its address inserted");
assert_eq!(created.credential, Some(credential));
assert_eq!(created.user.as_ref(), "deploy");
assert_eq!(created.auth, AuthKind::Password);
+60 -1
View File
@@ -479,6 +479,65 @@ async fn a_form_error_sits_above_the_dialog_and_a_click_puts_it_away(cx: &mut Te
});
}
#[gpui_kit::test]
async fn a_host_without_an_address_is_not_saved(cx: &mut TestAppContext) {
let (handle, workspace) = open_workspace_with_store(cx, HostStore::empty());
for (name, address) in [("", ""), ("", " "), ("web-01", "")] {
in_frame(cx, handle, |window, cx| window.click("new-host-panel", cx));
in_frame(cx, handle, |window, cx| {
window.click("host-name", cx);
window.input(name, cx);
window.click("host-address", cx);
window.input(address, cx);
window.click("commit", cx);
});
in_frame(cx, handle, |window, _| {
assert!(window.find("commit").visible());
assert_eq!(window.find("form-error").label(), Some("请输入地址"));
});
cx.update(|cx| assert!(workspace.read(cx).store().read(cx).hosts().is_empty()));
in_frame(cx, handle, |window, cx| window.press("escape", cx));
wait_for_dialog_to_close(cx, handle).await;
}
}
#[gpui_kit::test]
async fn a_blank_host_name_uses_the_address_and_is_saved(cx: &mut TestAppContext) {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("shellrs.db");
let store = HostStore::load(HostDatabase::open(&path).unwrap()).unwrap();
let (handle, workspace) = open_workspace_with_store(cx, store);
for (name, address) in [("", "example.com"), (" ", "10.0.0.2")] {
in_frame(cx, handle, |window, cx| {
// Saving closes the dialog and leaves focus outside the host panel.
window.click("host-search", cx);
window.click("new-host-panel", cx);
});
in_frame(cx, handle, |window, cx| {
window.click("host-name", cx);
window.input(name, cx);
window.click("host-address", cx);
window.input(&format!(" {address} "), cx);
window.click("commit", cx);
});
wait_for_dialog_to_close(cx, handle).await;
cx.update(|cx| {
let store = workspace.read(cx).store().read(cx);
let host = store
.hosts()
.iter()
.find(|host| host.address == address)
.unwrap();
assert_eq!(host.name.as_ref(), address);
});
}
let saved = HostDatabase::open(&path).unwrap().load().unwrap();
assert_eq!(saved.hosts.len(), 2);
for host in saved.hosts {
assert_eq!(host.name, host.address);
}
}
#[gpui_kit::test]
async fn new_host_dialog_validates_then_inserts(cx: &mut TestAppContext) {
let (handle, workspace) = open_workspace(cx);
@@ -527,7 +586,7 @@ async fn new_host_dialog_validates_then_inserts(cx: &mut TestAppContext) {
// The error pops up as a notification over the dialog, not as a line
// at the bottom of the form.
assert_eq!(window.notifications(cx).len(), 1);
assert_eq!(window.find("form-error").label(), Some("请输入名称"));
assert_eq!(window.find("form-error").label(), Some("请输入地址"));
window.click("commit", cx);
})
.unwrap();
+1 -1
View File
@@ -749,7 +749,7 @@ async fn a_dialog_says_what_is_wrong_in_a_notification_that_goes_with_it(cx: &mu
cx.run_until_parked();
for (open, field, error) in [
("new-host-panel", "host-name", "请输入名称"),
("new-host-panel", "host-address", "请输入地址"),
("temporary-connection", "host-address", "请输入地址"),
("new-group", "group-name", "请输入分组名称"),
("new-forward", "forward-name", "请选择端口转发经由的主机"),