Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions static/css/sftp-file-manager.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
35 changes: 35 additions & 0 deletions static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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} = {}) => {
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -1597,6 +1616,7 @@
keySelect.value = pendingKeyId;
selectedConnectionProfileState.pendingKeyId = null;
}
refreshConnectionProfileKeyResolution();
});

socket.on('key_uploaded', (data) => {
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -2127,6 +2149,7 @@
keySelect.addEventListener('change', () => {
const value = keySelect.value;
setFieldState(keySelect, keyHint, value ? '' : i18n.t('connection.selectSSHKey'), Boolean(value));
refreshConnectionProfileKeyResolution();
});
}

Expand All @@ -2144,6 +2167,7 @@
if (authTypeSelect.value === 'key' && keySelect) {
setFieldState(keySelect, keyHint, keySelect.value ? '' : i18n.t('connection.selectSSHKey'), Boolean(keySelect.value));
}
refreshConnectionProfileKeyResolution();
});
}
}
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion static/js/command-set-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
6 changes: 6 additions & 0 deletions static/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -7253,6 +7258,7 @@ const translations = {
'connection.savedContext': '已保存的连接',
'connection.reviewBeforeConnect': '检查设置并提供所需的凭据。',
'connection.jumpHostUnavailable': '已保存的跳板机不可用。请在下方选择替代项或确认直接连接。',
'connection.savedKeyUnavailable': '此已保存的连接没有可用的 SSH 密钥。请在连接前选择一个密钥。',
'connection.connectDirectlyInstead': '改为直接连接',
'connection.resolveJumpHost': '请选择跳板机或确认直接连接。',
'connection.newConnection': '快速连接',
Expand Down
9 changes: 5 additions & 4 deletions static/js/profile-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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';
Expand Down
24 changes: 12 additions & 12 deletions static/js/sftp-file-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -1208,13 +1208,13 @@ class SFTPFileManager {
</button>
</div>
<div class="fm-pane-toolbar" data-pane-toolbar="left">
<button type="button" data-pane-action="newfolder"><span class="material-icons" aria-hidden="true">create_new_folder</span><span data-i18n="fm.newFolder">New Folder</span></button>
<button type="button" data-pane-action="upload"><span class="material-icons" aria-hidden="true">upload</span><span data-i18n="fm.upload">Upload</span></button>
<button type="button" data-pane-action="download"><span class="material-icons" aria-hidden="true">download</span><span data-i18n="fm.download">Download</span></button>
<button type="button" data-pane-action="preview"><span class="material-icons" aria-hidden="true">preview</span><span data-i18n="fm.preview">Preview</span></button>
<button type="button" data-pane-action="rename"><span class="material-icons" aria-hidden="true">drive_file_rename_outline</span><span data-i18n="fm.rename">Rename</span></button>
<button type="button" data-pane-action="newfolder" aria-label="New Folder" title="New Folder" data-i18n-aria-label="fm.newFolder" data-i18n-title="fm.newFolder"><span class="material-icons" aria-hidden="true">create_new_folder</span><span data-i18n="fm.newFolder">New Folder</span></button>
<button type="button" data-pane-action="upload" aria-label="Upload" title="Upload" data-i18n-aria-label="fm.upload" data-i18n-title="fm.upload"><span class="material-icons" aria-hidden="true">upload</span><span data-i18n="fm.upload">Upload</span></button>
<button type="button" data-pane-action="download" aria-label="Download" title="Download" data-i18n-aria-label="fm.download" data-i18n-title="fm.download"><span class="material-icons" aria-hidden="true">download</span><span data-i18n="fm.download">Download</span></button>
<button type="button" data-pane-action="preview" aria-label="Preview" title="Preview" data-i18n-aria-label="fm.preview" data-i18n-title="fm.preview"><span class="material-icons" aria-hidden="true">preview</span><span data-i18n="fm.preview">Preview</span></button>
<button type="button" data-pane-action="rename" aria-label="Rename" title="Rename" data-i18n-aria-label="fm.rename" data-i18n-title="fm.rename"><span class="material-icons" aria-hidden="true">drive_file_rename_outline</span><span data-i18n="fm.rename">Rename</span></button>
<button type="button" data-pane-action="move" aria-label="Move…" title="Move…" data-i18n-aria-label="fm.moveAction" data-i18n-title="fm.moveAction"><span class="material-icons" aria-hidden="true">drive_file_move</span><span data-i18n="fm.moveAction">Move…</span></button>
<button type="button" class="is-danger" data-pane-action="delete"><span class="material-icons" aria-hidden="true">delete</span><span data-i18n="fm.delete">Delete</span></button>
<button type="button" class="is-danger" data-pane-action="delete" aria-label="Delete" title="Delete" data-i18n-aria-label="fm.delete" data-i18n-title="fm.delete"><span class="material-icons" aria-hidden="true">delete</span><span data-i18n="fm.delete">Delete</span></button>
</div>
<div class="fm-file-list-header">
<input type="checkbox" data-pane-select-all="left" aria-label="Select all files" data-i18n-aria-label="fm.workspace.selectAll">
Expand Down Expand Up @@ -1275,13 +1275,13 @@ class SFTPFileManager {
</button>
</div>
<div class="fm-pane-toolbar" data-pane-toolbar="right">
<button type="button" data-pane-action="newfolder"><span class="material-icons" aria-hidden="true">create_new_folder</span><span data-i18n="fm.newFolder">New Folder</span></button>
<button type="button" data-pane-action="upload"><span class="material-icons" aria-hidden="true">upload</span><span data-i18n="fm.upload">Upload</span></button>
<button type="button" data-pane-action="download"><span class="material-icons" aria-hidden="true">download</span><span data-i18n="fm.download">Download</span></button>
<button type="button" data-pane-action="preview"><span class="material-icons" aria-hidden="true">preview</span><span data-i18n="fm.preview">Preview</span></button>
<button type="button" data-pane-action="rename"><span class="material-icons" aria-hidden="true">drive_file_rename_outline</span><span data-i18n="fm.rename">Rename</span></button>
<button type="button" data-pane-action="newfolder" aria-label="New Folder" title="New Folder" data-i18n-aria-label="fm.newFolder" data-i18n-title="fm.newFolder"><span class="material-icons" aria-hidden="true">create_new_folder</span><span data-i18n="fm.newFolder">New Folder</span></button>
<button type="button" data-pane-action="upload" aria-label="Upload" title="Upload" data-i18n-aria-label="fm.upload" data-i18n-title="fm.upload"><span class="material-icons" aria-hidden="true">upload</span><span data-i18n="fm.upload">Upload</span></button>
<button type="button" data-pane-action="download" aria-label="Download" title="Download" data-i18n-aria-label="fm.download" data-i18n-title="fm.download"><span class="material-icons" aria-hidden="true">download</span><span data-i18n="fm.download">Download</span></button>
<button type="button" data-pane-action="preview" aria-label="Preview" title="Preview" data-i18n-aria-label="fm.preview" data-i18n-title="fm.preview"><span class="material-icons" aria-hidden="true">preview</span><span data-i18n="fm.preview">Preview</span></button>
<button type="button" data-pane-action="rename" aria-label="Rename" title="Rename" data-i18n-aria-label="fm.rename" data-i18n-title="fm.rename"><span class="material-icons" aria-hidden="true">drive_file_rename_outline</span><span data-i18n="fm.rename">Rename</span></button>
<button type="button" data-pane-action="move" aria-label="Move…" title="Move…" data-i18n-aria-label="fm.moveAction" data-i18n-title="fm.moveAction"><span class="material-icons" aria-hidden="true">drive_file_move</span><span data-i18n="fm.moveAction">Move…</span></button>
<button type="button" class="is-danger" data-pane-action="delete"><span class="material-icons" aria-hidden="true">delete</span><span data-i18n="fm.delete">Delete</span></button>
<button type="button" class="is-danger" data-pane-action="delete" aria-label="Delete" title="Delete" data-i18n-aria-label="fm.delete" data-i18n-title="fm.delete"><span class="material-icons" aria-hidden="true">delete</span><span data-i18n="fm.delete">Delete</span></button>
</div>
<div class="fm-file-list-header">
<input type="checkbox" data-pane-select-all="right" aria-label="Select all files" data-i18n-aria-label="fm.workspace.selectAll">
Expand Down
Loading
Loading