feat: ajouter les tags de session et les parametres par defaut de groupe
Tags : une session peut porter plusieurs tags libres (Session.tags), saisis comme une liste separee par des virgules dans l'editeur (normalises : espaces retires, doublons insensibles a la casse elimines), avec autocompletion sur les tags deja utilises. Affiches dans l'arbre des sessions, et filtrables via un champ dedie au-dessus de l'arbre (masque les sessions ne correspondant pas, sans toucher aux dossiers/terminaux locaux/hosts OpenSSH). Parametres de groupe : choix architectural deliberement plus simple qu'une resolution dynamique a la connexion. app/groups.py (GroupSettingsStore, meme pattern que les autres stores) associe un dossier (Session.group) a des valeurs par defaut (utilisateur, port, authentification, cle, bastion, keepalive). Ces valeurs ne s'appliquent qu'au moment de la creation d'une session, via un bouton explicite "↓ Heriter du groupe" dans SessionDialog - jamais de resolution automatique/retroactive. Ce choix evite d'introduire des valeurs sentinelles "herite vs. explicite" sur chaque champ (port=22 est a la fois une valeur par defaut ET une vraie valeur possible, par exemple) dans tout le reste du code (ConnectionController, SSHConnection...), pour une fonctionnalite que la priorisation du projet place parmi les secondaires. Une session reste ensuite un ensemble de valeurs normales, librement modifiables - "surcharger une valeur heritee" est donc trivialement satisfait. Clic droit sur un dossier dans le panneau lateral : "Nouvelle session dans ce dossier..." (pre-remplit le champ Dossier) et "Parametres du groupe..." (GroupSettingsDialog). tests/test_tags_and_groups.py : persistance des tags, CRUD GroupSettingsStore, normalisation de la saisie des tags, heritage applique/absent, filtrage dans l'arbre. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
213
app/dialogs/group_settings_dialog.py
Normal file
213
app/dialogs/group_settings_dialog.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""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 (
|
||||
QDialog, QFormLayout, QLineEdit, QSpinBox, QComboBox, QDialogButtonBox,
|
||||
QHBoxLayout, QPushButton, QWidget, QVBoxLayout, QCheckBox, QStackedWidget, QLabel,
|
||||
)
|
||||
|
||||
from app.session_manager import (
|
||||
SessionStore, BastionConfig, AUTH_AUTO, AUTH_KEY, AUTH_PASSWORD, AUTH_AGENT,
|
||||
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL,
|
||||
)
|
||||
from app.key_manager import KeyManager
|
||||
from app.groups import GroupSettings, GroupSettingsStore
|
||||
from app.openssh.config import load_openssh_hosts
|
||||
|
||||
|
||||
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 = QStackedWidget()
|
||||
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()
|
||||
Reference in New Issue
Block a user