chore: workover for ssh preferences

This commit is contained in:
2026-02-26 16:16:48 +01:00
parent 6a21476894
commit 74d6035f4a
11 changed files with 130 additions and 537 deletions

View File

@@ -1,106 +0,0 @@
.credentials-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.credentials-tabs {
display: flex;
gap: 4px;
padding: 8px 12px;
border-bottom: 1px solid var(--vscode-panel-border);
background-color: var(--vscode-sideBar-background);
}
.credentials-tabs button {
padding: 6px 12px;
background-color: transparent;
border: none;
border-radius: 4px;
color: var(--vscode-foreground);
font-size: 12px;
cursor: pointer;
transition: background-color 0.15s;
}
.credentials-tabs button:hover {
background-color: var(--vscode-toolbar-hoverBackground);
}
.credentials-tabs button.active {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.credentials-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.credentials-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.credentials-header h4 {
margin: 0 0 8px;
font-size: 16px;
font-weight: 600;
color: var(--vscode-foreground);
}
.credentials-header p {
font-size: 12px;
margin: 0;
line-height: 1.5;
}
.credentials-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.credentials-field label {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
font-weight: 500;
color: var(--vscode-descriptionForeground);
}
.credentials-field input {
padding: 8px 12px;
border-radius: 4px;
font-size: 13px;
}
.toggle-visibility {
background: none;
border: none;
padding: 2px 4px;
cursor: pointer;
font-size: 14px;
opacity: 0.7;
transition: opacity 0.15s;
}
.toggle-visibility:hover {
opacity: 1;
}
.credentials-actions {
display: flex;
gap: 8px;
margin-top: 8px;
padding-top: 16px;
border-top: 1px solid var(--vscode-panel-border);
}
.credentials-actions button {
padding: 8px 16px;
font-size: 12px;
}

View File

@@ -1,207 +0,0 @@
import React, { useState, useEffect } from 'react';
import { showToast } from '../Toast';
import { useI18n } from '../../i18n';
import './CredentialsPanel.css';
interface Credentials {
ftpHost?: string;
ftpUser?: string;
ftpPassword?: string;
sshHost?: string;
sshUser?: string;
sshKeyPath?: string;
}
export const CredentialsPanel: React.FC = () => {
const { t: tr } = useI18n();
const [credentials, setCredentials] = useState<Credentials>({
ftpHost: '',
ftpUser: '',
ftpPassword: '',
sshHost: '',
sshUser: '',
sshKeyPath: '',
});
const [activeTab, setActiveTab] = useState<'ftp' | 'ssh'>('ftp');
const [showTokens, _setShowTokens] = useState(false);
// Load saved credentials (in a real app, use secure storage)
useEffect(() => {
const loadCredentials = async () => {
try {
const savedCreds = localStorage.getItem('bds-credentials');
if (savedCreds) {
setCredentials(JSON.parse(savedCreds));
}
} catch (error) {
console.error(tr('credentials.error.load'), error);
}
};
loadCredentials();
}, []);
const handleSave = async () => {
try {
// Save to localStorage (in production, use secure storage)
localStorage.setItem('bds-credentials', JSON.stringify(credentials));
showToast.success(tr('credentials.toast.saved'));
} catch (error) {
console.error(tr('credentials.error.save'), error);
showToast.error(tr('credentials.toast.saveFailed'));
}
};
const handleClear = (type: 'ftp' | 'ssh') => {
const newCreds = { ...credentials };
switch (type) {
case 'ftp':
newCreds.ftpHost = '';
newCreds.ftpUser = '';
newCreds.ftpPassword = '';
break;
case 'ssh':
newCreds.sshHost = '';
newCreds.sshUser = '';
newCreds.sshKeyPath = '';
break;
}
setCredentials(newCreds);
};
const handleTestConnection = async (type: 'ftp' | 'ssh') => {
showToast.loading(tr('credentials.toast.testing', { type: type.toUpperCase() }));
// Simulate connection test
await new Promise(resolve => setTimeout(resolve, 1500));
// In a real implementation, this would test the actual connection
showToast.dismiss();
showToast.error(tr('credentials.toast.connectionFailed'));
};
return (
<div className="credentials-panel">
<div className="credentials-tabs">
<button
className={activeTab === 'ftp' ? 'active' : ''}
onClick={() => setActiveTab('ftp')}
>
{tr('credentials.tab.ftp')}
</button>
<button
className={activeTab === 'ssh' ? 'active' : ''}
onClick={() => setActiveTab('ssh')}
>
{tr('credentials.tab.ssh')}
</button>
</div>
<div className="credentials-content">
{activeTab === 'ftp' && (
<div className="credentials-form">
<div className="credentials-header">
<h4>{tr('credentials.ftp.title')}</h4>
<p className="text-muted">
{tr('credentials.ftp.description')}
</p>
</div>
<div className="credentials-field">
<label>{tr('credentials.field.host')}</label>
<input
type="text"
placeholder={tr('credentials.ftp.placeholder.host')}
value={credentials.ftpHost}
onChange={(e) => setCredentials({ ...credentials, ftpHost: e.target.value })}
/>
</div>
<div className="credentials-field">
<label>{tr('credentials.field.username')}</label>
<input
type="text"
placeholder={tr('credentials.ftp.placeholder.username')}
value={credentials.ftpUser}
onChange={(e) => setCredentials({ ...credentials, ftpUser: e.target.value })}
/>
</div>
<div className="credentials-field">
<label>{tr('credentials.field.password')}</label>
<input
type={showTokens ? 'text' : 'password'}
placeholder={tr('credentials.ftp.placeholder.password')}
value={credentials.ftpPassword}
onChange={(e) => setCredentials({ ...credentials, ftpPassword: e.target.value })}
/>
</div>
<div className="credentials-actions">
<button onClick={handleSave}>{tr('common.save')}</button>
<button className="secondary" onClick={() => handleTestConnection('ftp')}>
{tr('credentials.action.testConnection')}
</button>
<button className="secondary danger" onClick={() => handleClear('ftp')}>
{tr('common.clear')}
</button>
</div>
</div>
)}
{activeTab === 'ssh' && (
<div className="credentials-form">
<div className="credentials-header">
<h4>{tr('credentials.ssh.title')}</h4>
<p className="text-muted">
{tr('credentials.ssh.description')}
</p>
</div>
<div className="credentials-field">
<label>{tr('credentials.field.host')}</label>
<input
type="text"
placeholder={tr('credentials.ssh.placeholder.host')}
value={credentials.sshHost}
onChange={(e) => setCredentials({ ...credentials, sshHost: e.target.value })}
/>
</div>
<div className="credentials-field">
<label>{tr('credentials.field.username')}</label>
<input
type="text"
placeholder={tr('credentials.ssh.placeholder.username')}
value={credentials.sshUser}
onChange={(e) => setCredentials({ ...credentials, sshUser: e.target.value })}
/>
</div>
<div className="credentials-field">
<label>{tr('credentials.field.sshKeyPath')}</label>
<input
type="text"
placeholder={tr('credentials.ssh.placeholder.keyPath')}
value={credentials.sshKeyPath}
onChange={(e) => setCredentials({ ...credentials, sshKeyPath: e.target.value })}
/>
</div>
<div className="credentials-actions">
<button onClick={handleSave}>{tr('common.save')}</button>
<button className="secondary" onClick={() => handleTestConnection('ssh')}>
{tr('credentials.action.testConnection')}
</button>
<button className="secondary danger" onClick={() => handleClear('ssh')}>
{tr('common.clear')}
</button>
</div>
</div>
)}
</div>
</div>
);
};
export default CredentialsPanel;

View File

@@ -1 +0,0 @@
export { CredentialsPanel } from './CredentialsPanel';

View File

@@ -257,6 +257,22 @@
opacity: 1;
}
/* Info banner */
.setting-info-banner {
padding: 10px 16px;
margin: 0 0 4px;
background: var(--vscode-editorInfo-background, rgba(0, 120, 212, 0.1));
border-left: 3px solid var(--vscode-editorInfo-foreground, #3794ff);
border-radius: 3px;
font-size: 12px;
line-height: 1.5;
color: var(--vscode-foreground);
}
.setting-info-banner p {
margin: 0;
}
/* Action buttons */
.setting-actions {
display: flex;

View File

@@ -23,14 +23,11 @@ export const scrollToSettingsSection = (category: SettingsCategory) => {
// Settings categories
interface Credentials {
// FTP Publishing
ftpHost: string;
ftpUser: string;
ftpPassword: string;
// SSH Publishing
sshHost: string;
sshUser: string;
sshKeyPath: string;
sshRemotePath: string;
sshMode: 'scp' | 'rsync';
}
interface CategoryMetadata {
@@ -48,12 +45,10 @@ const RENDER_LANGUAGE_LABEL_KEY: Record<SupportedLanguage, string> = {
};
const defaultCredentials: Credentials = {
ftpHost: '',
ftpUser: '',
ftpPassword: '',
sshHost: '',
sshUser: '',
sshKeyPath: '',
sshRemotePath: '',
sshMode: 'scp',
};
// Search icon for the search bar
@@ -137,7 +132,6 @@ export const SettingsView: React.FC = () => {
} = useAppStore();
const [searchQuery, setSearchQuery] = useState('');
const [credentials, setCredentials] = useState<Credentials>(defaultCredentials);
const [showSecrets, setShowSecrets] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
// Project settings
@@ -304,23 +298,11 @@ export const SettingsView: React.FC = () => {
}
};
const handleClearCredentials = (type: 'ftp' | 'ssh') => {
const newCreds = { ...credentials };
switch (type) {
case 'ftp':
newCreds.ftpHost = '';
newCreds.ftpUser = '';
newCreds.ftpPassword = '';
break;
case 'ssh':
newCreds.sshHost = '';
newCreds.sshUser = '';
newCreds.sshKeyPath = '';
break;
}
const handleClearCredentials = () => {
const newCreds = { ...credentials, sshHost: '', sshUser: '', sshRemotePath: '', sshMode: 'scp' as const };
setCredentials(newCreds);
localStorage.setItem('bds-credentials', JSON.stringify(newCreds));
showToast.success(t('settings.toast.credentialsCleared', { type: type.toUpperCase() }));
showToast.success(t('settings.toast.credentialsCleared', { type: 'SSH' }));
};
// Save project settings
@@ -395,7 +377,7 @@ export const SettingsView: React.FC = () => {
const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
const aiKeywords = ['ai', 'assistant', 'chat', 'model', 'prompt', 'system', 'api', 'key', 'claude', 'gpt', 'opencode'];
const technologyKeywords = ['technology', 'python', 'runtime', 'worker', 'webworker', 'main thread', 'execution'];
const publishingKeywords = ['publishing', 'ftp', 'ssh', 'deploy', 'server', 'host', 'upload'];
const publishingKeywords = ['publishing', 'ssh', 'deploy', 'server', 'host', 'upload', 'scp', 'rsync'];
const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'scripts', 'links', 'folder', 'filesystem'];
const renderProjectSettings = () => (
@@ -1054,123 +1036,78 @@ export const SettingsView: React.FC = () => {
);
const renderPublishingSettings = () => (
<>
<SettingSection
id="settings-section-publishing"
title={t('settings.publishing.ftpTitle')}
description={t('credentials.ftp.description')}
hidden={!sectionHasMatches(publishingKeywords)}
<SettingSection
id="settings-section-publishing"
title={t('settings.publishing.sshTitle')}
description={t('credentials.ssh.description')}
hidden={!sectionHasMatches(publishingKeywords)}
>
<div className="setting-info-banner">
<p>{t('settings.publishing.sshKeyAuthNotice')}</p>
</div>
<SettingRow
id="ssh-mode"
label={t('settings.publishing.sshModeLabel')}
description={t('settings.publishing.sshModeDescription')}
>
<SettingRow
id="ftp-host"
label={t('credentials.field.host')}
description={t('settings.publishing.ftpHostDescription')}
<select
id="ssh-mode"
value={credentials.sshMode}
onChange={(e) => setCredentials({ ...credentials, sshMode: e.target.value as 'scp' | 'rsync' })}
>
<input
id="ftp-host"
type="text"
placeholder={t('credentials.ftp.placeholder.host')}
value={credentials.ftpHost}
onChange={(e) => setCredentials({ ...credentials, ftpHost: e.target.value })}
/>
</SettingRow>
<option value="scp">{t('settings.publishing.sshMode.scp')}</option>
<option value="rsync">{t('settings.publishing.sshMode.rsync')}</option>
</select>
</SettingRow>
<SettingRow
id="ftp-user"
label={t('credentials.field.username')}
description={t('settings.publishing.ftpUsernameDescription')}
>
<input
id="ftp-user"
type="text"
placeholder={t('credentials.ftp.placeholder.username')}
value={credentials.ftpUser}
onChange={(e) => setCredentials({ ...credentials, ftpUser: e.target.value })}
/>
</SettingRow>
<SettingRow
id="ftp-password"
label={t('credentials.field.password')}
description={t('settings.publishing.ftpPasswordDescription')}
>
<div className="setting-input-group">
<input
id="ftp-password"
type={showSecrets ? 'text' : 'password'}
placeholder={t('credentials.ftp.placeholder.password')}
value={credentials.ftpPassword}
onChange={(e) => setCredentials({ ...credentials, ftpPassword: e.target.value })}
/>
<button
className="setting-toggle-visibility"
onClick={() => setShowSecrets(!showSecrets)}
title={showSecrets ? t('settings.publishing.hidePassword') : t('settings.publishing.showPassword')}
>
{showSecrets ? '🔒' : '👁'}
</button>
</div>
</SettingRow>
<div className="setting-actions">
<button className="primary" onClick={handleSavePublishing}>{t('common.save')}</button>
<button className="secondary danger" onClick={() => handleClearCredentials('ftp')}>{t('common.clear')}</button>
</div>
</SettingSection>
<SettingSection
title={t('settings.publishing.sshTitle')}
description={t('credentials.ssh.description')}
hidden={!sectionHasMatches(publishingKeywords)}
<SettingRow
id="ssh-host"
label={t('credentials.field.host')}
description={t('settings.publishing.sshHostDescription')}
>
<SettingRow
<input
id="ssh-host"
label={t('credentials.field.host')}
description={t('settings.publishing.sshHostDescription')}
>
<input
id="ssh-host"
type="text"
placeholder={t('credentials.ssh.placeholder.host')}
value={credentials.sshHost}
onChange={(e) => setCredentials({ ...credentials, sshHost: e.target.value })}
/>
</SettingRow>
type="text"
placeholder={t('credentials.ssh.placeholder.host')}
value={credentials.sshHost}
onChange={(e) => setCredentials({ ...credentials, sshHost: e.target.value })}
/>
</SettingRow>
<SettingRow
<SettingRow
id="ssh-user"
label={t('credentials.field.username')}
description={t('settings.publishing.sshUsernameDescription')}
>
<input
id="ssh-user"
label={t('credentials.field.username')}
description={t('settings.publishing.sshUsernameDescription')}
>
<input
id="ssh-user"
type="text"
placeholder={t('credentials.ssh.placeholder.username')}
value={credentials.sshUser}
onChange={(e) => setCredentials({ ...credentials, sshUser: e.target.value })}
/>
</SettingRow>
type="text"
placeholder={t('credentials.ssh.placeholder.username')}
value={credentials.sshUser}
onChange={(e) => setCredentials({ ...credentials, sshUser: e.target.value })}
/>
</SettingRow>
<SettingRow
id="ssh-keypath"
label={t('credentials.field.sshKeyPath')}
description={t('settings.publishing.sshKeyPathDescription')}
>
<input
id="ssh-keypath"
type="text"
placeholder={t('credentials.ssh.placeholder.keyPath')}
value={credentials.sshKeyPath}
onChange={(e) => setCredentials({ ...credentials, sshKeyPath: e.target.value })}
/>
</SettingRow>
<SettingRow
id="ssh-remote-path"
label={t('credentials.field.sshRemotePath')}
description={t('settings.publishing.sshRemotePathDescription')}
>
<input
id="ssh-remote-path"
type="text"
placeholder={t('credentials.ssh.placeholder.remotePath')}
value={credentials.sshRemotePath}
onChange={(e) => setCredentials({ ...credentials, sshRemotePath: e.target.value })}
/>
</SettingRow>
<div className="setting-actions">
<button className="primary" onClick={handleSavePublishing}>{t('common.save')}</button>
<button className="secondary danger" onClick={() => handleClearCredentials('ssh')}>{t('common.clear')}</button>
</div>
</SettingSection>
</>
<div className="setting-actions">
<button className="primary" onClick={handleSavePublishing}>{t('common.save')}</button>
<button className="secondary danger" onClick={handleClearCredentials}>{t('common.clear')}</button>
</div>
</SettingSection>
);
const renderDataSettings = () => (

View File

@@ -10,7 +10,6 @@ export { MilkdownEditor } from './MilkdownEditor';
export { Lightbox, ImageGallery, useMarkdownImages } from './Lightbox';
export { TaskPopup } from './TaskPopup';
export { ResizablePanel } from './ResizablePanel';
export { CredentialsPanel } from './CredentialsPanel';
export { SettingsView } from './SettingsView';
export { StyleView } from './StyleView';
export { TagsView, scrollToTagsSection, type TagsCategory } from './TagsView';