Signale par l'utilisateur : le dialogue de "SFTP sur cette connexion" ne
tenait pas sur l'ecran. Deux causes distinctes, verifiees par capture d'ecran
avant de conclure (rendu hors-ecran + grab()) :
1. Bug reel, dans les deux dialogues qui ont une section Bastion (SessionDialog
et GroupSettingsDialog) : leur QStackedWidget reservait par defaut la
hauteur de sa PLUS GRANDE page (Configuration manuelle, 5 champs) meme
quand la page vide "Aucun" etait affichee - environ 200px de vide pur,
visible sur la premiere capture. Nouveau app/ui/auto_height_stack.py
(AutoHeightStackedWidget) : le sizeHint suit la page COURANTE, pas la plus
grande de toutes: -186px des l'ouverture, a bastion desactive.
2. Choix d'ergonomie : une connexion rapide sert a aller vite (souvent un
serveur de test en DHCP, cf. le README) - Bastion/Keepalive/Reconnexion y
sont maintenant repliees par defaut derriere un bouton "Options
avancees...", toujours accessibles mais qui n'allongent plus le dialogue
pour le cas courant. Se deplient automatiquement si "Heriter du groupe"
ramene un bastion herite, pour ne pas cacher un changement silencieux.
Le dialogue complet ("Nouvelle session" depuis le panneau lateral) est
inchange : tout reste visible d'emblee, c'est une operation reflechie, pas
une connexion a la volee.
Resultat mesure : dialogue de connexion rapide 1026px -> 492px replie (-52%),
876px si depliees (au lieu de 1026, meme contenu, sans le vide). Le dialogue
"Nouvelle session" complet passe de meme a 816px grace au seul correctif (1).
tests/test_session_dialog_compact.py (5 verifications) : sizeHint de
AutoHeightStackedWidget sur la page courante, repli par defaut en connexion
rapide, redimensionnement au depli, absence de repli hors connexion rapide,
depli automatique sur heritage de groupe. Suite complete revalidee (35/35),
ruff propre.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
233 lines
10 KiB
Python
233 lines
10 KiB
Python
"""Valeurs par defaut d'un groupe : appliquees en pre-remplissage a la creation
|
|
d'une nouvelle session dans ce dossier (voir app.groups)."""
|
|
from __future__ import annotations
|
|
|
|
from PySide6.QtWidgets import (
|
|
QCheckBox,
|
|
QComboBox,
|
|
QDialog,
|
|
QDialogButtonBox,
|
|
QFormLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QPushButton,
|
|
QSpinBox,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from app.groups import GroupSettings, GroupSettingsStore
|
|
from app.key_manager import KeyManager
|
|
from app.openssh.config import load_openssh_hosts
|
|
from app.session_manager import (
|
|
AUTH_AGENT,
|
|
AUTH_AUTO,
|
|
AUTH_KEY,
|
|
AUTH_PASSWORD,
|
|
BASTION_MANUAL,
|
|
BASTION_NONE,
|
|
BASTION_OPENSSH,
|
|
BASTION_SESSION,
|
|
BastionConfig,
|
|
SessionStore,
|
|
)
|
|
from app.ui.auto_height_stack import AutoHeightStackedWidget
|
|
|
|
|
|
class GroupSettingsDialog(QDialog):
|
|
"""Meme principe que la section Bastion de SessionDialog, en plus compact :
|
|
ce sont des valeurs par defaut, pas la configuration d'une connexion reelle."""
|
|
|
|
def __init__(
|
|
self, group_path: str, store: GroupSettingsStore, key_manager: KeyManager,
|
|
session_store: SessionStore, parent=None,
|
|
):
|
|
super().__init__(parent)
|
|
self.group_path = group_path
|
|
self.store = store
|
|
self.key_manager = key_manager
|
|
self.session_store = session_store
|
|
self.settings = store.get(group_path) or GroupSettings(path=group_path)
|
|
self.setWindowTitle(f"Parametres par defaut — {group_path}")
|
|
self.setMinimumWidth(420)
|
|
|
|
layout = QVBoxLayout(self)
|
|
info = QLabel(
|
|
"Ces valeurs pre-remplissent une nouvelle session creee dans ce dossier "
|
|
"(bouton « Heriter du groupe » dans son editeur). Elles ne changent jamais "
|
|
"les sessions deja creees."
|
|
)
|
|
info.setWordWrap(True)
|
|
layout.addWidget(info)
|
|
|
|
form = QFormLayout()
|
|
self.username_edit = QLineEdit(self.settings.username)
|
|
self.port_spin = QSpinBox()
|
|
self.port_spin.setRange(1, 65535)
|
|
self.port_spin.setValue(self.settings.port or 22)
|
|
self.auth_combo = QComboBox()
|
|
self.auth_combo.addItem("Automatique (comme la commande ssh)", AUTH_AUTO)
|
|
self.auth_combo.addItem("Cle SSH choisie", AUTH_KEY)
|
|
self.auth_combo.addItem("Mot de passe (saisi a la connexion)", AUTH_PASSWORD)
|
|
self.auth_combo.addItem("Agent SSH", AUTH_AGENT)
|
|
idx = self.auth_combo.findData(self.settings.auth_type)
|
|
self.auth_combo.setCurrentIndex(idx if idx >= 0 else 0)
|
|
|
|
self.key_combo = QComboBox()
|
|
for entry in self.key_manager.keys:
|
|
self.key_combo.addItem(f"{entry.name} ({entry.key_type})", entry.private_path)
|
|
self.browse_key_btn = QPushButton("Parcourir...")
|
|
key_row = QWidget()
|
|
key_layout = QHBoxLayout(key_row)
|
|
key_layout.setContentsMargins(0, 0, 0, 0)
|
|
key_layout.addWidget(self.key_combo, 1)
|
|
key_layout.addWidget(self.browse_key_btn)
|
|
if self.settings.key_path:
|
|
existing = self.key_combo.findData(self.settings.key_path)
|
|
if existing >= 0:
|
|
self.key_combo.setCurrentIndex(existing)
|
|
else:
|
|
self.key_combo.addItem(self.settings.key_path, self.settings.key_path)
|
|
self.key_combo.setCurrentIndex(self.key_combo.count() - 1)
|
|
|
|
form.addRow("Utilisateur :", self.username_edit)
|
|
form.addRow("Port :", self.port_spin)
|
|
form.addRow("Authentification :", self.auth_combo)
|
|
form.addRow("Cle privee :", key_row)
|
|
layout.addLayout(form)
|
|
|
|
layout.addWidget(self._build_bastion_group())
|
|
|
|
keepalive_form = QFormLayout()
|
|
self.keepalive_check = QCheckBox("Keepalive actif")
|
|
self.keepalive_check.setChecked(self.settings.keepalive_enabled)
|
|
self.keepalive_interval_spin = QSpinBox()
|
|
self.keepalive_interval_spin.setRange(5, 3600)
|
|
self.keepalive_interval_spin.setSuffix(" s")
|
|
self.keepalive_interval_spin.setValue(self.settings.keepalive_interval or 30)
|
|
self.keepalive_count_spin = QSpinBox()
|
|
self.keepalive_count_spin.setRange(1, 20)
|
|
self.keepalive_count_spin.setValue(self.settings.keepalive_count_max or 3)
|
|
keepalive_form.addRow(self.keepalive_check)
|
|
keepalive_form.addRow("Intervalle :", self.keepalive_interval_spin)
|
|
keepalive_form.addRow("Echecs avant abandon :", self.keepalive_count_spin)
|
|
layout.addLayout(keepalive_form)
|
|
|
|
self.auth_combo.currentIndexChanged.connect(self._update_key_row)
|
|
self.browse_key_btn.clicked.connect(self._browse_key)
|
|
self._update_key_row()
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(self._on_accept)
|
|
buttons.rejected.connect(self.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
def _update_key_row(self) -> None:
|
|
is_key_auth = self.auth_combo.currentData() == AUTH_KEY
|
|
self.key_combo.setEnabled(is_key_auth)
|
|
self.browse_key_btn.setEnabled(is_key_auth)
|
|
|
|
def _browse_key(self) -> None:
|
|
from PySide6.QtWidgets import QFileDialog
|
|
path, _ = QFileDialog.getOpenFileName(self, "Choisir une cle privee")
|
|
if path:
|
|
self.key_combo.addItem(path, path)
|
|
self.key_combo.setCurrentIndex(self.key_combo.count() - 1)
|
|
|
|
# ---------- Bastion (identique dans l'esprit a SessionDialog, en plus compact) ----------
|
|
def _build_bastion_group(self):
|
|
from PySide6.QtWidgets import QGroupBox
|
|
bastion = self.settings.bastion
|
|
group = QGroupBox("Bastion / Jump Host par defaut")
|
|
v = QVBoxLayout(group)
|
|
|
|
self.bastion_mode_combo = QComboBox()
|
|
self.bastion_mode_combo.addItem("Aucun", BASTION_NONE)
|
|
self.bastion_mode_combo.addItem("Une session PySSH existante", BASTION_SESSION)
|
|
self.bastion_mode_combo.addItem("Un Host de ~/.ssh/config", BASTION_OPENSSH)
|
|
self.bastion_mode_combo.addItem("Configuration manuelle", BASTION_MANUAL)
|
|
v.addWidget(self.bastion_mode_combo)
|
|
|
|
self.bastion_stack = AutoHeightStackedWidget()
|
|
v.addWidget(self.bastion_stack)
|
|
self.bastion_stack.addWidget(QWidget())
|
|
|
|
session_page = QWidget()
|
|
session_form = QFormLayout(session_page)
|
|
session_form.setContentsMargins(0, 4, 0, 0)
|
|
self.bastion_session_combo = QComboBox()
|
|
for s in sorted(self.session_store.sessions, key=lambda s: s.name.lower()):
|
|
self.bastion_session_combo.addItem(f"{s.name} ({s.username}@{s.host})", s.id)
|
|
if self.bastion_session_combo.count() == 0:
|
|
self.bastion_session_combo.addItem("(aucune session enregistree)", "")
|
|
self.bastion_session_combo.setEnabled(False)
|
|
session_form.addRow("Session :", self.bastion_session_combo)
|
|
self.bastion_stack.addWidget(session_page)
|
|
|
|
openssh_page = QWidget()
|
|
openssh_form = QFormLayout(openssh_page)
|
|
openssh_form.setContentsMargins(0, 4, 0, 0)
|
|
self.bastion_openssh_combo = QComboBox()
|
|
hosts = load_openssh_hosts()
|
|
for h in hosts:
|
|
self.bastion_openssh_combo.addItem(f"{h.alias} ({h.display_target})", h.alias)
|
|
if not hosts:
|
|
self.bastion_openssh_combo.addItem("(aucun host dans ~/.ssh/config)", "")
|
|
self.bastion_openssh_combo.setEnabled(False)
|
|
openssh_form.addRow("Host :", self.bastion_openssh_combo)
|
|
self.bastion_stack.addWidget(openssh_page)
|
|
|
|
manual_page = QWidget()
|
|
manual_form = QFormLayout(manual_page)
|
|
manual_form.setContentsMargins(0, 4, 0, 0)
|
|
self.bastion_host_edit = QLineEdit(bastion.host if bastion.mode == BASTION_MANUAL else "")
|
|
self.bastion_port_spin = QSpinBox()
|
|
self.bastion_port_spin.setRange(1, 65535)
|
|
self.bastion_port_spin.setValue(bastion.port or 22)
|
|
self.bastion_user_edit = QLineEdit(bastion.username if bastion.mode == BASTION_MANUAL else "")
|
|
manual_form.addRow("Hote :", self.bastion_host_edit)
|
|
manual_form.addRow("Port :", self.bastion_port_spin)
|
|
manual_form.addRow("Utilisateur :", self.bastion_user_edit)
|
|
self.bastion_stack.addWidget(manual_page)
|
|
|
|
self.bastion_mode_combo.currentIndexChanged.connect(self.bastion_stack.setCurrentIndex)
|
|
mode_idx = self.bastion_mode_combo.findData(bastion.mode)
|
|
self.bastion_mode_combo.setCurrentIndex(mode_idx if mode_idx >= 0 else 0)
|
|
self.bastion_stack.setCurrentIndex(self.bastion_mode_combo.currentIndex())
|
|
if bastion.mode == BASTION_SESSION:
|
|
found = self.bastion_session_combo.findData(bastion.session_id)
|
|
if found >= 0:
|
|
self.bastion_session_combo.setCurrentIndex(found)
|
|
elif bastion.mode == BASTION_OPENSSH:
|
|
found = self.bastion_openssh_combo.findData(bastion.openssh_alias)
|
|
if found >= 0:
|
|
self.bastion_openssh_combo.setCurrentIndex(found)
|
|
return group
|
|
|
|
def _result_bastion(self) -> BastionConfig:
|
|
mode = self.bastion_mode_combo.currentData()
|
|
bastion = BastionConfig(mode=mode)
|
|
if mode == BASTION_SESSION:
|
|
bastion.session_id = self.bastion_session_combo.currentData() or ""
|
|
elif mode == BASTION_OPENSSH:
|
|
bastion.openssh_alias = self.bastion_openssh_combo.currentData() or ""
|
|
elif mode == BASTION_MANUAL:
|
|
bastion.host = self.bastion_host_edit.text().strip()
|
|
bastion.port = self.bastion_port_spin.value()
|
|
bastion.username = self.bastion_user_edit.text().strip()
|
|
return bastion
|
|
|
|
def _on_accept(self) -> None:
|
|
self.settings.path = self.group_path
|
|
self.settings.username = self.username_edit.text().strip()
|
|
self.settings.port = self.port_spin.value()
|
|
self.settings.auth_type = self.auth_combo.currentData()
|
|
self.settings.key_path = self.key_combo.currentData() or ""
|
|
self.settings.bastion = self._result_bastion()
|
|
self.settings.keepalive_enabled = self.keepalive_check.isChecked()
|
|
self.settings.keepalive_interval = self.keepalive_interval_spin.value()
|
|
self.settings.keepalive_count_max = self.keepalive_count_spin.value()
|
|
self.store.set(self.settings)
|
|
self.accept()
|