Allow removing unreachable remotes from session manager
This commit is contained in:
@@ -131,6 +131,10 @@ Session”;二次确认后会在后台停止远端 Herdr Session、中断其
|
|||||||
记忆记录,不关闭已有 window,也不停止远端 Herdr server。手机端单击条目即可执行,
|
记忆记录,不关闭已有 window,也不停止远端 Herdr server。手机端单击条目即可执行,
|
||||||
键盘支持上下键、Enter、输入搜索、Backspace、`Ctrl+R` 刷新和 Esc 返回/关闭。
|
键盘支持上下键、Enter、输入搜索、Backspace、`Ctrl+R` 刷新和 Esc 返回/关闭。
|
||||||
|
|
||||||
|
远端查询失败时,该远端行支持 Enter 重试,也支持按 `d` 或 `D` 二次确认后从 Hub 的
|
||||||
|
历史列表移除。这只删除本机保存的远端登记,不会连接或修改不可达主机上的数据;需要时
|
||||||
|
可再次通过“添加远端”登记。
|
||||||
|
|
||||||
选中已停止的命名远端 Session 后,按 `d` 或 `D` 会进入二次确认;无论是否仍有关联
|
选中已停止的命名远端 Session 后,按 `d` 或 `D` 会进入二次确认;无论是否仍有关联
|
||||||
Hub Tab 都可以删除。可点击“确认删除”、按 Enter,或再按一次 `d`/`D`。确认后在后台
|
Hub Tab 都可以删除。可点击“确认删除”、按 Enter,或再按一次 `d`/`D`。确认后在后台
|
||||||
通过 SSH 执行 `herdr session delete`,永久删除该 Session 的工作现场和历史数据;若仍有
|
通过 SSH 执行 `herdr session delete`,永久删除该 Session 的工作现场和历史数据;若仍有
|
||||||
|
|||||||
+19
-5
@@ -412,7 +412,7 @@ def build_main_rows(
|
|||||||
PickerRow(
|
PickerRow(
|
||||||
f"retry:{entry.target}",
|
f"retry:{entry.target}",
|
||||||
"retry",
|
"retry",
|
||||||
"! 查询失败 · 单击重试",
|
"! 查询失败 · Enter 重试 · d/D 移除记录",
|
||||||
result.error,
|
result.error,
|
||||||
target=entry.target,
|
target=entry.target,
|
||||||
)
|
)
|
||||||
@@ -759,11 +759,20 @@ class SessionSelector:
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
return self._activate(row)
|
return self._activate(row)
|
||||||
|
if key in {"d", "D"} and self.view == "confirm-remove":
|
||||||
|
row = next(
|
||||||
|
(item for item in rows if item.kind == "confirm-yes"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
return self._activate(row)
|
||||||
if key in {"d", "D"} and self.view == "main":
|
if key in {"d", "D"} and self.view == "main":
|
||||||
row = self._selected_row(rows)
|
row = self._selected_row(rows)
|
||||||
if row is not None and row.deletable:
|
if row is not None and row.deletable:
|
||||||
self._show_delete_session_confirmation(row)
|
self._show_delete_session_confirmation(row)
|
||||||
return None
|
return None
|
||||||
|
if row is not None and row.kind == "retry" and row.target is not None:
|
||||||
|
self._show_remove_confirmation(row.target)
|
||||||
|
return None
|
||||||
if row is not None and row.target is not None and row.session == "default":
|
if row is not None and row.target is not None and row.session == "default":
|
||||||
self.message = "Herdr 不支持删除 default Session"
|
self.message = "Herdr 不支持删除 default Session"
|
||||||
else:
|
else:
|
||||||
@@ -902,9 +911,8 @@ class SessionSelector:
|
|||||||
self.input_value = self.aliases.get(row.endpoint_key, "")
|
self.input_value = self.aliases.get(row.endpoint_key, "")
|
||||||
self.message = ""
|
self.message = ""
|
||||||
elif row.kind == "remove":
|
elif row.kind == "remove":
|
||||||
self.view = "confirm-remove"
|
assert row.target is not None
|
||||||
self.input_target = row.target
|
self._show_remove_confirmation(row.target)
|
||||||
self.selected_key = "confirm-no"
|
|
||||||
elif row.kind == "stop-session":
|
elif row.kind == "stop-session":
|
||||||
assert (
|
assert (
|
||||||
row.window_id is not None
|
row.window_id is not None
|
||||||
@@ -1012,6 +1020,12 @@ class SessionSelector:
|
|||||||
self.selected_key = "delete-session-no"
|
self.selected_key = "delete-session-no"
|
||||||
self.message = ""
|
self.message = ""
|
||||||
|
|
||||||
|
def _show_remove_confirmation(self, target: str) -> None:
|
||||||
|
self.view = "confirm-remove"
|
||||||
|
self.input_target = target
|
||||||
|
self.selected_key = "confirm-no"
|
||||||
|
self.message = ""
|
||||||
|
|
||||||
def _finish_delete_session(
|
def _finish_delete_session(
|
||||||
self,
|
self,
|
||||||
target: str,
|
target: str,
|
||||||
@@ -1243,7 +1257,7 @@ class SessionSelector:
|
|||||||
line = f" {main} {detail}"
|
line = f" {main} {detail}"
|
||||||
self._safe_add(screen, y, 0, line, attribute)
|
self._safe_add(screen, y, 0, line, attribute)
|
||||||
|
|
||||||
footer = "Enter 打开 · d/D 删除停止项 · Ctrl+R 刷新 · Esc 关闭"
|
footer = "Enter 打开/重试 · d/D 删除/移除 · Ctrl+R 刷新 · Esc 关闭"
|
||||||
if self.view in {"working-delete-session", "working-stop-session"}:
|
if self.view in {"working-delete-session", "working-stop-session"}:
|
||||||
footer = "远端操作进行中,请稍候"
|
footer = "远端操作进行中,请稍候"
|
||||||
elif self.view != "main":
|
elif self.view != "main":
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ class SelectorCatalogTests(unittest.TestCase):
|
|||||||
set(),
|
set(),
|
||||||
)
|
)
|
||||||
self.assertTrue(any(row.kind == "retry" for row in failed))
|
self.assertTrue(any(row.kind == "retry" for row in failed))
|
||||||
|
retry = next(row for row in failed if row.kind == "retry")
|
||||||
|
self.assertIn("d/D 移除记录", retry.main)
|
||||||
matches = filter_rows(failed, "offline")
|
matches = filter_rows(failed, "offline")
|
||||||
self.assertEqual([row.kind for row in matches], ["retry"])
|
self.assertEqual([row.kind for row in matches], ["retry"])
|
||||||
|
|
||||||
@@ -277,6 +279,34 @@ class SelectorCatalogTests(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class SelectorActionTests(unittest.TestCase):
|
class SelectorActionTests(unittest.TestCase):
|
||||||
|
def test_unreachable_remote_can_be_removed_with_delete_key(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
registry = RemoteRegistry(Path(directory) / "remotes.json")
|
||||||
|
entry = RemoteEntry("tang1@10.8.0.7")
|
||||||
|
registry.upsert(entry)
|
||||||
|
selector = SessionSelector([], registry, "/missing/ssh")
|
||||||
|
self.addCleanup(selector.pool.close)
|
||||||
|
self.addCleanup(selector.action_executor.shutdown, wait=True)
|
||||||
|
selector.results[entry.target] = RemoteQueryResult(
|
||||||
|
entry,
|
||||||
|
error="ssh: connect to host 10.8.0.7 port 22: Connection timed out",
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = selector._rows() # type: ignore[attr-defined]
|
||||||
|
retry = next(item for item in rows if item.kind == "retry")
|
||||||
|
selector.selected_key = retry.key
|
||||||
|
selector._handle_key("D", rows) # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
self.assertEqual(selector.view, "confirm-remove")
|
||||||
|
self.assertEqual(selector.input_target, entry.target)
|
||||||
|
confirm_rows = selector._rows() # type: ignore[attr-defined]
|
||||||
|
selector._handle_key("d", confirm_rows) # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
self.assertEqual(selector.view, "main")
|
||||||
|
self.assertEqual(registry.load(), [])
|
||||||
|
self.assertNotIn(entry.target, selector.entries)
|
||||||
|
self.assertIn("已移除", selector.message)
|
||||||
|
|
||||||
def test_remote_session_can_be_stopped_and_closed_without_closing_popup(self) -> None:
|
def test_remote_session_can_be_stopped_and_closed_without_closing_popup(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
registry = RemoteRegistry(Path(directory) / "remotes.json")
|
registry = RemoteRegistry(Path(directory) / "remotes.json")
|
||||||
|
|||||||
Reference in New Issue
Block a user