From f20e374aca14a3d77537bd110311fae4be75fd2f Mon Sep 17 00:00:00 2001 From: bifrost0x Date: Sat, 26 Sep 2026 15:20:24 +0200 Subject: [PATCH 1/2] Improve connection review and file workspace usability --- static/css/sftp-file-manager.css | 19 ++++++++ static/css/style.css | 1 + static/js/app.js | 17 ++++++++ static/js/command-set-manager.js | 3 +- static/js/i18n.js | 6 +++ static/js/profile-manager.js | 9 ++-- static/js/sftp-file-manager.js | 24 +++++------ templates/index.html | 1 + tests/e2e/file-workspace.spec.js | 55 ++++++++++++++++++++++++ tests/e2e/product-captures.spec.js | 4 +- tests/e2e/profile-launcher.spec.js | 54 +++++++++++++++++++++++ tests/e2e/quick-connect-redesign.spec.js | 7 +++ 12 files changed, 181 insertions(+), 19 deletions(-) diff --git a/static/css/sftp-file-manager.css b/static/css/sftp-file-manager.css index 06bdc4d..250443c 100644 --- a/static/css/sftp-file-manager.css +++ b/static/css/sftp-file-manager.css @@ -2296,6 +2296,25 @@ font-size: 17px; } +@media (min-width: 768px) { + #sftpFileManager.fm-workspace-split .fm-pane-toolbar button:nth-child(n + 3) { + min-width: 34px; + justify-content: center; + padding: 0 6px; + } + + #sftpFileManager.fm-workspace-split .fm-pane-toolbar button:nth-child(n + 3) span:not(.material-icons) { + display: none; + } +} + +@media (any-pointer: coarse) { + #sftpFileManager.fm-workspace-split .fm-pane-toolbar button:nth-child(n + 3) { + min-width: 44px; + min-height: 44px; + } +} + .fm-file-list-header, #sftpFileManager.fm-workspace-mode .fm-file-item { display: grid; diff --git a/static/css/style.css b/static/css/style.css index 34eb371..8a9e1ae 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -5077,6 +5077,7 @@ body.keyboard-open.notepad-focused .notepad-panel { line-height: 1.4; } +.connection-profile-key-resolution, .connection-profile-jump-resolution { display: grid; gap: 8px; diff --git a/static/js/app.js b/static/js/app.js index 3cea541..2c41091 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -303,6 +303,15 @@ let selectedConnectionProfileState = null; const pendingReconnectSessionMap = new Map(); + function refreshConnectionProfileKeyResolution() { + const needsKey = selectedConnectionProfileState?.requiresKey === true + && ProfileManager.keysLoaded === true + && document.getElementById('authTypeSelect')?.value === 'key' + && !document.getElementById('keySelect')?.value; + document.getElementById('connectionProfileKeyResolution') + ?.classList.toggle('hidden', !needsKey); + } + function refreshConnectionProfileJumpResolution() { const resolution = document.getElementById('connectionProfileJumpResolution'); const directConfirm = document.getElementById('connectionProfileDirectConfirm'); @@ -337,6 +346,7 @@ const directConfirm = document.getElementById('connectionProfileDirectConfirm'); if (directConfirm) directConfirm.checked = false; document.getElementById('connectionProfileJumpResolution')?.classList.add('hidden'); + document.getElementById('connectionProfileKeyResolution')?.classList.add('hidden'); }; window.prepareSessionReconnectJump = (session, {applyToForm = false} = {}) => { @@ -381,6 +391,8 @@ if (!container) return; const history = this.getHistory(); + document.getElementById('recentConnectionsCard') + ?.classList.toggle('hidden', history.length === 0); container.replaceChildren(); container.classList.toggle('hidden', history.length === 0); document.getElementById('recentConnectionsEmpty') @@ -1597,6 +1609,7 @@ keySelect.value = pendingKeyId; selectedConnectionProfileState.pendingKeyId = null; } + refreshConnectionProfileKeyResolution(); }); socket.on('key_uploaded', (data) => { @@ -1792,6 +1805,7 @@ const requiredJumpHostId = profile.jump_host_id || ''; selectedConnectionProfileState = { profileId, + requiresKey: profile.auth_type === 'key', pendingKeyId: profile.auth_type === 'key' ? profile.key_id || null : null, missingJumpHost: Boolean( requiredJumpHostId @@ -1806,6 +1820,7 @@ profileContext?.classList.remove('hidden'); const profileContextName = document.getElementById('connectionProfileContextName'); if (profileContextName) profileContextName.textContent = profile.name || profile.host; + refreshConnectionProfileKeyResolution(); refreshConnectionProfileJumpResolution(); return profile; } @@ -2127,6 +2142,7 @@ keySelect.addEventListener('change', () => { const value = keySelect.value; setFieldState(keySelect, keyHint, value ? '' : i18n.t('connection.selectSSHKey'), Boolean(value)); + refreshConnectionProfileKeyResolution(); }); } @@ -2144,6 +2160,7 @@ if (authTypeSelect.value === 'key' && keySelect) { setFieldState(keySelect, keyHint, keySelect.value ? '' : i18n.t('connection.selectSSHKey'), Boolean(keySelect.value)); } + refreshConnectionProfileKeyResolution(); }); } } diff --git a/static/js/command-set-manager.js b/static/js/command-set-manager.js index 659f103..e511eb7 100644 --- a/static/js/command-set-manager.js +++ b/static/js/command-set-manager.js @@ -328,7 +328,8 @@ window.CommandSetManager = { menu.className = 'command-set-action-menu'; const summary = document.createElement('summary'); summary.className = 'btn btn-secondary btn-sm material-icons'; - summary.setAttribute('aria-label', this.t('common.actions', 'Actions')); + summary.setAttribute('aria-label', + `${this.t('common.actions', 'Actions')} ${commandSet.name}`); summary.textContent = 'more_horiz'; const menuItems = document.createElement('div'); menuItems.className = 'command-set-action-menu-items'; diff --git a/static/js/i18n.js b/static/js/i18n.js index 1349149..8ff57d3 100644 --- a/static/js/i18n.js +++ b/static/js/i18n.js @@ -165,6 +165,7 @@ const translations = { 'connection.savedContext': 'Saved connection', 'connection.reviewBeforeConnect': 'Review the settings and provide any required credentials.', 'connection.jumpHostUnavailable': 'The saved jump host is unavailable. Choose a replacement below or confirm a direct connection.', + 'connection.savedKeyUnavailable': 'This saved connection has no available SSH key. Select a key before connecting.', 'connection.connectDirectlyInstead': 'Connect directly instead', 'connection.resolveJumpHost': 'Choose a jump host or confirm a direct connection.', 'connection.newConnection': 'Quick Connect', @@ -1584,6 +1585,7 @@ const translations = { 'connection.savedContext': 'Kết nối đã lưu', 'connection.reviewBeforeConnect': 'Kiểm tra cài đặt và nhập thông tin xác thực bắt buộc.', 'connection.jumpHostUnavailable': 'Máy chủ trung chuyển đã lưu không khả dụng. Chọn máy chủ thay thế bên dưới hoặc xác nhận kết nối trực tiếp.', + 'connection.savedKeyUnavailable': 'Kết nối đã lưu này không có khóa SSH khả dụng. Hãy chọn một khóa trước khi kết nối.', 'connection.connectDirectlyInstead': 'Thay vào đó, kết nối trực tiếp', 'connection.resolveJumpHost': 'Chọn máy chủ trung chuyển hoặc xác nhận kết nối trực tiếp.', 'connection.newConnection': 'Kết nối nhanh', @@ -3002,6 +3004,7 @@ const translations = { 'connection.savedContext': 'Gespeicherte Verbindung', 'connection.reviewBeforeConnect': 'Prüfe die Einstellungen und ergänze erforderliche Zugangsdaten.', 'connection.jumpHostUnavailable': 'Der gespeicherte Jump Host ist nicht verfügbar. Wähle unten einen Ersatz oder bestätige eine direkte Verbindung.', + 'connection.savedKeyUnavailable': 'Für diese gespeicherte Verbindung ist kein SSH-Schlüssel verfügbar. Wähle vor dem Verbinden einen Schlüssel aus.', 'connection.connectDirectlyInstead': 'Stattdessen direkt verbinden', 'connection.resolveJumpHost': 'Wähle einen Jump Host oder bestätige eine direkte Verbindung.', 'connection.newConnection': 'Schnellverbindung', @@ -4419,6 +4422,7 @@ const translations = { 'connection.savedContext': 'Connexion enregistrée', 'connection.reviewBeforeConnect': 'Vérifiez les paramètres et fournissez les identifiants requis.', 'connection.jumpHostUnavailable': 'L’hôte de rebond enregistré est indisponible. Choisissez un remplacement ci-dessous ou confirmez une connexion directe.', + 'connection.savedKeyUnavailable': 'Aucune clé SSH n’est disponible pour cette connexion enregistrée. Sélectionnez une clé avant de vous connecter.', 'connection.connectDirectlyInstead': 'Se connecter directement à la place', 'connection.resolveJumpHost': 'Choisissez un hôte de rebond ou confirmez une connexion directe.', 'connection.newConnection': 'Connexion rapide', @@ -5836,6 +5840,7 @@ const translations = { 'connection.savedContext': 'Conexión guardada', 'connection.reviewBeforeConnect': 'Revisa la configuración e introduce las credenciales necesarias.', 'connection.jumpHostUnavailable': 'El host de salto guardado no está disponible. Elige otro abajo o confirma una conexión directa.', + 'connection.savedKeyUnavailable': 'Esta conexión guardada no tiene una clave SSH disponible. Selecciona una clave antes de conectarte.', 'connection.connectDirectlyInstead': 'Conectar directamente en su lugar', 'connection.resolveJumpHost': 'Elige un host de salto o confirma una conexión directa.', 'connection.newConnection': 'Conexión rápida', @@ -7253,6 +7258,7 @@ const translations = { 'connection.savedContext': '已保存的连接', 'connection.reviewBeforeConnect': '检查设置并提供所需的凭据。', 'connection.jumpHostUnavailable': '已保存的跳板机不可用。请在下方选择替代项或确认直接连接。', + 'connection.savedKeyUnavailable': '此已保存的连接没有可用的 SSH 密钥。请在连接前选择一个密钥。', 'connection.connectDirectlyInstead': '改为直接连接', 'connection.resolveJumpHost': '请选择跳板机或确认直接连接。', 'connection.newConnection': '快速连接', diff --git a/static/js/profile-manager.js b/static/js/profile-manager.js index e29ed64..e820439 100644 --- a/static/js/profile-manager.js +++ b/static/js/profile-manager.js @@ -755,9 +755,8 @@ const ProfileManager = { this.handleAuthTypeChange(profile.auth_type); - if (profile.auth_type === 'key' && profile.key_id) { - document.getElementById('keySelect').value = profile.key_id; - } + document.getElementById('keySelect').value = + profile.auth_type === 'key' ? profile.key_id || '' : ''; // Jump host (bastion) reference — the password is entered at connect time. const jumpHostSelect = document.getElementById('jumpHostSelect'); @@ -1119,13 +1118,15 @@ const ProfileManager = { connect.dataset.profileAction = 'connect'; connect.dataset.profileId = profile.id; connect.textContent = this.t('connection.connect', 'Connect'); + connect.setAttribute('aria-label', `${connect.textContent} ${profile.name || profile.host}`); actions.appendChild(connect); const actionMenu = document.createElement('details'); actionMenu.className = 'profile-action-menu'; const actionSummary = document.createElement('summary'); actionSummary.className = 'btn btn-secondary btn-sm material-icons'; - actionSummary.setAttribute('aria-label', this.t('common.actions', 'Actions')); + actionSummary.setAttribute('aria-label', + `${this.t('common.actions', 'Actions')} ${profile.name || profile.host}`); actionSummary.textContent = 'more_horiz'; const actionMenuItems = document.createElement('div'); actionMenuItems.className = 'profile-action-menu-items'; diff --git a/static/js/sftp-file-manager.js b/static/js/sftp-file-manager.js index b0e0b83..b29104d 100644 --- a/static/js/sftp-file-manager.js +++ b/static/js/sftp-file-manager.js @@ -1208,13 +1208,13 @@ class SFTPFileManager {
- - - - - + + + + + - +
@@ -1275,13 +1275,13 @@ class SFTPFileManager {
- - - - - + + + + + - +
diff --git a/templates/index.html b/templates/index.html index 1e53334..98f4aed 100644 --- a/templates/index.html +++ b/templates/index.html @@ -506,6 +506,7 @@

Connection Detail Saved connection Review the settings and provide any required credentials. +