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..077e2a7 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -303,6 +303,22 @@ let selectedConnectionProfileState = null; const pendingReconnectSessionMap = new Map(); + function hasUsableConnectionKey(keyId) { + return Boolean(keyId) && ProfileManager.keys.some( + key => key.id === keyId && key.usable === true + ); + } + + function refreshConnectionProfileKeyResolution() { + const keyId = document.getElementById('keySelect')?.value; + const needsKey = selectedConnectionProfileState?.requiresKey === true + && ProfileManager.keysLoaded === true + && document.getElementById('authTypeSelect')?.value === 'key' + && !hasUsableConnectionKey(keyId); + document.getElementById('connectionProfileKeyResolution') + ?.classList.toggle('hidden', !needsKey); + } + function refreshConnectionProfileJumpResolution() { const resolution = document.getElementById('connectionProfileJumpResolution'); const directConfirm = document.getElementById('connectionProfileDirectConfirm'); @@ -337,6 +353,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 +398,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 +1616,7 @@ keySelect.value = pendingKeyId; selectedConnectionProfileState.pendingKeyId = null; } + refreshConnectionProfileKeyResolution(); }); socket.on('key_uploaded', (data) => { @@ -1792,6 +1812,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 +1827,7 @@ profileContext?.classList.remove('hidden'); const profileContextName = document.getElementById('connectionProfileContextName'); if (profileContextName) profileContextName.textContent = profile.name || profile.host; + refreshConnectionProfileKeyResolution(); refreshConnectionProfileJumpResolution(); return profile; } @@ -2127,6 +2149,7 @@ keySelect.addEventListener('change', () => { const value = keySelect.value; setFieldState(keySelect, keyHint, value ? '' : i18n.t('connection.selectSSHKey'), Boolean(value)); + refreshConnectionProfileKeyResolution(); }); } @@ -2144,6 +2167,7 @@ if (authTypeSelect.value === 'key' && keySelect) { setFieldState(keySelect, keyHint, keySelect.value ? '' : i18n.t('connection.selectSSHKey'), Boolean(keySelect.value)); } + refreshConnectionProfileKeyResolution(); }); } } @@ -2662,6 +2686,17 @@ return; } + if (authType === 'key' && ProfileManager.keysLoaded + && !hasUsableConnectionKey(keyId)) { + showNotification( + window.i18n?.t('connection.readinessKey', 'Key missing or unavailable') + || 'Key missing or unavailable', + 'error', + ); + document.getElementById('keySelect').focus(); + return; + } + if (refreshConnectionProfileJumpResolution()) { showNotification( window.i18n?.t( 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. +