feat: multi-type server support — SQL, Redis, Grafana, Prometheus, Telnet, WinRM, RDP/VNC
Full implementation of multi-type server management across GUI and CLI: New clients: SQLClient (MariaDB/MSSQL/PostgreSQL), RedisClient, GrafanaClient, PrometheusClient, TelnetSession, WinRMClient, RemoteDesktopLauncher. New GUI tabs: QueryTab (SQL editor + Treeview), RedisTab (console + history), GrafanaTab (dashboards + alerts), PrometheusTab (PromQL + targets), PowershellTab (PS/CMD), LaunchTab (RDP/VNC external client). Infrastructure: TAB_REGISTRY for conditional tabs per server type, adaptive server_dialog fields, colored type badges in sidebar, status checker for all types (SSH/TCP/SQL/Redis/HTTP), 100+ i18n keys. CLI: ssh.py extended with --sql, --redis, --grafana-*, --prom-*, --ps, --cmd. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
258
gui/app.py
258
gui/app.py
@@ -20,6 +20,43 @@ from gui.tabs.info_tab import InfoTab
|
||||
from gui.tabs.keys_tab import KeysTab
|
||||
from gui.tabs.setup_tab import SetupTab
|
||||
from gui.tabs.totp_tab import TOTPTab
|
||||
from gui.tabs.query_tab import QueryTab
|
||||
from gui.tabs.redis_tab import RedisTab
|
||||
from gui.tabs.grafana_tab import GrafanaTab
|
||||
from gui.tabs.prometheus_tab import PrometheusTab
|
||||
from gui.tabs.powershell_tab import PowershellTab
|
||||
from gui.tabs.launch_tab import LaunchTab
|
||||
|
||||
# Tab sets per server type — determines which tabs are shown
|
||||
TAB_REGISTRY = {
|
||||
"ssh": ["terminal", "files", "info", "keys", "totp", "setup"],
|
||||
"telnet": ["terminal", "info", "setup"],
|
||||
"winrm": ["powershell", "info", "setup"],
|
||||
"mariadb": ["query", "info", "setup"],
|
||||
"mssql": ["query", "info", "setup"],
|
||||
"postgresql": ["query", "info", "setup"],
|
||||
"redis": ["console", "info", "setup"],
|
||||
"grafana": ["dashboards", "info", "setup"],
|
||||
"prometheus": ["metrics", "info", "setup"],
|
||||
"rdp": ["launch", "info", "setup"],
|
||||
"vnc": ["launch", "info", "setup"],
|
||||
}
|
||||
|
||||
# Map tab key → widget class (used as lazy factory)
|
||||
TAB_CLASSES = {
|
||||
"terminal": TerminalTab,
|
||||
"files": FilesTab,
|
||||
"info": InfoTab,
|
||||
"keys": KeysTab,
|
||||
"totp": TOTPTab,
|
||||
"setup": SetupTab,
|
||||
"query": QueryTab,
|
||||
"console": RedisTab,
|
||||
"dashboards": GrafanaTab,
|
||||
"metrics": PrometheusTab,
|
||||
"powershell": PowershellTab,
|
||||
"launch": LaunchTab,
|
||||
}
|
||||
|
||||
|
||||
class App(ctk.CTk):
|
||||
@@ -67,11 +104,11 @@ class App(ctk.CTk):
|
||||
self.sidebar.delete_callback = self._delete_server
|
||||
|
||||
# Main area
|
||||
main = ctk.CTkFrame(self, fg_color="transparent")
|
||||
main.pack(side="right", fill="both", expand=True)
|
||||
self._main_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
self._main_frame.pack(side="right", fill="both", expand=True)
|
||||
|
||||
# Header bar (language + about)
|
||||
header_bar = ctk.CTkFrame(main, fg_color="transparent", height=40)
|
||||
header_bar = ctk.CTkFrame(self._main_frame, fg_color="transparent", height=40)
|
||||
header_bar.pack(fill="x", padx=10, pady=(8, 0))
|
||||
header_bar.pack_propagate(False)
|
||||
|
||||
@@ -93,39 +130,96 @@ class App(ctk.CTk):
|
||||
)
|
||||
self.about_btn.pack(side="right", padx=(5, 5))
|
||||
|
||||
# Tabview
|
||||
self.tabview = ctk.CTkTabview(main, command=self._on_tab_changed)
|
||||
# Initialize tab tracking
|
||||
self.tabview = None
|
||||
self._tab_keys = []
|
||||
self._tab_instances = {}
|
||||
|
||||
# Build default SSH tab set
|
||||
self._rebuild_tabs(TAB_REGISTRY["ssh"])
|
||||
|
||||
def _rebuild_tabs(self, tab_keys: list[str], restore_tab_key: str | None = None):
|
||||
"""Destroy current tabview and rebuild with the given tab keys."""
|
||||
# Remember current active tab
|
||||
if restore_tab_key is None:
|
||||
restore_tab_key = self._get_current_tab_key() if self._tab_keys else None
|
||||
|
||||
# Destroy old tab instances
|
||||
for key, widget in self._tab_instances.items():
|
||||
try:
|
||||
widget.pack_forget()
|
||||
widget.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self._tab_instances = {}
|
||||
|
||||
# Destroy old tabview
|
||||
if self.tabview is not None:
|
||||
try:
|
||||
self.tabview.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Store new tab key list
|
||||
self._tab_keys = list(tab_keys)
|
||||
|
||||
# Create new tabview
|
||||
self.tabview = ctk.CTkTabview(self._main_frame, command=self._on_tab_changed)
|
||||
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
# Tab names stored for language updates
|
||||
self._tab_keys = ["terminal", "files", "info", "keys", "totp", "setup"]
|
||||
for key in self._tab_keys:
|
||||
self.tabview.add(t(key))
|
||||
|
||||
self.terminal_tab = TerminalTab(self.tabview.tab(t("terminal")), self.store, self.session_pool)
|
||||
self.terminal_tab.pack(fill="both", expand=True)
|
||||
# Create tab instances using TAB_CLASSES factory
|
||||
for key in self._tab_keys:
|
||||
cls = TAB_CLASSES.get(key)
|
||||
if cls is None:
|
||||
continue
|
||||
parent = self.tabview.tab(t(key))
|
||||
widget = self._create_tab_instance(cls, key, parent)
|
||||
widget.pack(fill="both", expand=True)
|
||||
self._tab_instances[key] = widget
|
||||
|
||||
self.files_tab = FilesTab(self.tabview.tab(t("files")), self.store, self.session_pool)
|
||||
self.files_tab.pack(fill="both", expand=True)
|
||||
# Restore previously active tab if still available
|
||||
if restore_tab_key and restore_tab_key in self._tab_keys:
|
||||
try:
|
||||
self.tabview.set(t(restore_tab_key))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.info_tab = InfoTab(self.tabview.tab(t("info")), self.store, edit_callback=self._edit_server)
|
||||
self.info_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.keys_tab = KeysTab(self.tabview.tab(t("keys")), self.store)
|
||||
self.keys_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.totp_tab = TOTPTab(self.tabview.tab(t("totp")), self.store)
|
||||
self.totp_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.setup_tab = SetupTab(self.tabview.tab(t("setup")), self.store)
|
||||
self.setup_tab.pack(fill="both", expand=True)
|
||||
def _create_tab_instance(self, cls, key: str, parent):
|
||||
"""Create a tab widget instance with the correct constructor args."""
|
||||
if cls in (TerminalTab, FilesTab):
|
||||
return cls(parent, self.store, self.session_pool)
|
||||
elif cls is InfoTab:
|
||||
return cls(parent, self.store, edit_callback=self._edit_server)
|
||||
elif cls is SetupTab:
|
||||
return cls(parent, self.store)
|
||||
elif cls in (KeysTab, TOTPTab):
|
||||
return cls(parent, self.store)
|
||||
else:
|
||||
# QueryTab, RedisTab, GrafanaTab, PrometheusTab, PowershellTab, LaunchTab
|
||||
return cls(parent, self.store)
|
||||
|
||||
def _on_server_select(self, alias: str):
|
||||
self.terminal_tab.set_server(alias)
|
||||
self.files_tab.set_server(alias)
|
||||
self.info_tab.set_server(alias)
|
||||
self.keys_tab.set_server(alias)
|
||||
self.totp_tab.set_server(alias)
|
||||
# Determine server type and required tabs
|
||||
if alias:
|
||||
server = self.store.get_server(alias)
|
||||
server_type = server.get("type", "ssh") if server else "ssh"
|
||||
else:
|
||||
server_type = "ssh"
|
||||
|
||||
new_tab_keys = TAB_REGISTRY.get(server_type, TAB_REGISTRY["ssh"])
|
||||
|
||||
# Rebuild tabs only if the tab set changed
|
||||
if new_tab_keys != self._tab_keys:
|
||||
self._rebuild_tabs(new_tab_keys)
|
||||
|
||||
# Notify each tab instance about the selected server
|
||||
for key, widget in self._tab_instances.items():
|
||||
if hasattr(widget, "set_server"):
|
||||
widget.set_server(alias)
|
||||
|
||||
# Update session indicators after a short delay (connection is async)
|
||||
self.after(1500, self.sidebar.update_session_indicators)
|
||||
|
||||
@@ -143,7 +237,9 @@ class App(ctk.CTk):
|
||||
self.sidebar._select(new_alias)
|
||||
self.session_pool.rename_server(alias, new_alias)
|
||||
else:
|
||||
self.info_tab.refresh()
|
||||
info = self._tab_instances.get("info")
|
||||
if info and hasattr(info, "refresh"):
|
||||
info.refresh()
|
||||
|
||||
def _delete_server(self, alias: str):
|
||||
if messagebox.askyesno(t("delete_server"), t("delete_confirm").format(alias=alias)):
|
||||
@@ -155,7 +251,9 @@ class App(ctk.CTk):
|
||||
def _on_status_update(self):
|
||||
self.sidebar.update_statuses()
|
||||
self.sidebar.update_session_indicators()
|
||||
self.info_tab.refresh()
|
||||
info = self._tab_instances.get("info")
|
||||
if info and hasattr(info, "refresh"):
|
||||
info.refresh()
|
||||
|
||||
def _show_about(self):
|
||||
AboutDialog(self)
|
||||
@@ -190,76 +288,42 @@ class App(ctk.CTk):
|
||||
# Remember selected server
|
||||
alias = self.sidebar.get_selected()
|
||||
# Use provided key or default to first tab
|
||||
current_key = restore_tab_key or self._tab_keys[0]
|
||||
current_key = restore_tab_key or (self._tab_keys[0] if self._tab_keys else "terminal")
|
||||
|
||||
# Save state before destroying tabs
|
||||
saved_remote_path = self.files_tab._remote_path
|
||||
saved_local_path = self.files_tab._local_path
|
||||
had_sftp = self.files_tab._sftp is not None and self.files_tab._sftp.connected
|
||||
# Save FilesTab state if it exists
|
||||
files_tab = self._tab_instances.get("files")
|
||||
saved_remote_path = None
|
||||
saved_local_path = None
|
||||
had_sftp = False
|
||||
if files_tab:
|
||||
saved_remote_path = files_tab._remote_path
|
||||
saved_local_path = files_tab._local_path
|
||||
had_sftp = files_tab._sftp is not None and files_tab._sftp.connected
|
||||
|
||||
# Disconnect all sessions in the pool
|
||||
self.session_pool.disconnect_all()
|
||||
|
||||
# Detach tab contents
|
||||
self.terminal_tab.pack_forget()
|
||||
self.files_tab.pack_forget()
|
||||
self.info_tab.pack_forget()
|
||||
self.keys_tab.pack_forget()
|
||||
self.totp_tab.pack_forget()
|
||||
self.setup_tab.pack_forget()
|
||||
# Rebuild tabs with translated names (same tab keys, just new language)
|
||||
self._rebuild_tabs(self._tab_keys, restore_tab_key=current_key)
|
||||
|
||||
# Get the main frame and destroy old tabview
|
||||
main = self.tabview.master
|
||||
self.tabview.destroy()
|
||||
# Restore FilesTab state if it exists in new tab set
|
||||
files_tab = self._tab_instances.get("files")
|
||||
if files_tab:
|
||||
files_tab._local_path = saved_local_path
|
||||
files_tab._refresh_local()
|
||||
if alias and had_sftp:
|
||||
files_tab._remote_path = saved_remote_path
|
||||
files_tab.set_server(alias)
|
||||
elif alias:
|
||||
files_tab.set_server(alias)
|
||||
|
||||
# Create new tabview with translated names
|
||||
self.tabview = ctk.CTkTabview(main, command=self._on_tab_changed)
|
||||
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
for key in self._tab_keys:
|
||||
self.tabview.add(t(key))
|
||||
|
||||
# Re-parent tab contents
|
||||
self.terminal_tab = TerminalTab(self.tabview.tab(t("terminal")), self.store, self.session_pool)
|
||||
self.terminal_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.files_tab = FilesTab(self.tabview.tab(t("files")), self.store, self.session_pool)
|
||||
self.files_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.info_tab = InfoTab(self.tabview.tab(t("info")), self.store, edit_callback=self._edit_server)
|
||||
self.info_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.keys_tab = KeysTab(self.tabview.tab(t("keys")), self.store)
|
||||
self.keys_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.totp_tab = TOTPTab(self.tabview.tab(t("totp")), self.store)
|
||||
self.totp_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.setup_tab = SetupTab(self.tabview.tab(t("setup")), self.store)
|
||||
self.setup_tab.pack(fill="both", expand=True)
|
||||
|
||||
# Restore active tab by key
|
||||
try:
|
||||
self.tabview.set(t(current_key))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Restore file paths and reconnect properly
|
||||
self.files_tab._local_path = saved_local_path
|
||||
self.files_tab._refresh_local()
|
||||
if alias and had_sftp:
|
||||
# Had active SFTP — reconnect and restore remote path
|
||||
self.files_tab._remote_path = saved_remote_path
|
||||
self.files_tab.set_server(alias)
|
||||
elif alias:
|
||||
self.files_tab.set_server(alias)
|
||||
|
||||
# Restore server selection for other tabs (terminal auto-reconnects)
|
||||
# Restore server selection for all other tabs
|
||||
if alias:
|
||||
self.terminal_tab.set_server(alias)
|
||||
self.info_tab.set_server(alias)
|
||||
self.keys_tab.set_server(alias)
|
||||
self.totp_tab.set_server(alias)
|
||||
for key, widget in self._tab_instances.items():
|
||||
if key == "files":
|
||||
continue # Already handled above
|
||||
if hasattr(widget, "set_server"):
|
||||
widget.set_server(alias)
|
||||
|
||||
# Update sidebar
|
||||
self.sidebar.update_language()
|
||||
@@ -367,14 +431,22 @@ class App(ctk.CTk):
|
||||
"""Handle tab switch — manage terminal focus."""
|
||||
try:
|
||||
current = self.tabview.get()
|
||||
if current == t("terminal"):
|
||||
self.terminal_tab._terminal.focus_terminal()
|
||||
terminal = self._tab_instances.get("terminal")
|
||||
if terminal and current == t("terminal"):
|
||||
terminal._terminal.focus_terminal()
|
||||
else:
|
||||
self.focus_set()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_close(self):
|
||||
# Clean up tab instances
|
||||
for key, widget in self._tab_instances.items():
|
||||
if hasattr(widget, "on_close"):
|
||||
try:
|
||||
widget.on_close()
|
||||
except Exception:
|
||||
pass
|
||||
# Disconnect all sessions before closing
|
||||
self.session_pool.disconnect_all()
|
||||
self.checker.stop()
|
||||
|
||||
Reference in New Issue
Block a user