/** * WorkspaceSection - Settings section for managing saved SSH connection profiles. * * Provides CRUD UI for: * - Listing saved SSH profiles * - Adding new profiles (name, host, port, username, auth method) * - Inline editing existing profile fields * - Deleting profiles with confirmation * * Profile changes persist via ConfigManager and trigger context list refresh. */ import { useCallback, useEffect, useState } from 'react'; import { useAppTranslation } from '@features/localization/renderer'; import { api } from '@renderer/api'; import { confirm } from '@renderer/components/common/ConfirmDialog'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useStore } from '@renderer/store'; import { Edit2, Loader2, Plus, Save, Server, Trash2, X } from 'lucide-react'; import { SettingsSectionHeader } from '../components/SettingsSectionHeader'; import { SettingsSelect } from '../components/SettingsSelect'; import type { SshAuthMethod, SshConnectionProfile } from '@shared/types'; const inputClass = 'w-full rounded-md border px-3 py-1.5 text-sm focus:outline-none focus:ring-1'; const inputStyle = { backgroundColor: 'var(--color-surface-raised)', borderColor: 'var(--color-border)', color: 'var(--color-text)', }; const authMethodOptionValues: readonly SshAuthMethod[] = [ 'auto', 'agent', 'privateKey', 'password', ]; const authMethodLabelKeys = { agent: 'workspaceProfiles.authMethods.agent', auto: 'workspaceProfiles.authMethods.auto', // eslint-disable-next-line sonarjs/no-hardcoded-passwords -- SSH auth method label key, not a credential. password: 'workspaceProfiles.authMethods.password', privateKey: 'workspaceProfiles.authMethods.privateKey', } as const satisfies Record; const defaultForm = { name: '', host: '', port: '22', username: '', authMethod: 'auto' as SshAuthMethod, privateKeyPath: '', }; export const WorkspaceSection = (): React.JSX.Element => { const { t } = useAppTranslation('settings'); const [profiles, setProfiles] = useState([]); const [loading, setLoading] = useState(true); const [editingId, setEditingId] = useState(null); const [showAddForm, setShowAddForm] = useState(false); const authMethodOptions = authMethodOptionValues.map((value) => ({ value, label: t(authMethodLabelKeys[value]), })); // Form state const [formName, setFormName] = useState(defaultForm.name); const [formHost, setFormHost] = useState(defaultForm.host); const [formPort, setFormPort] = useState(defaultForm.port); const [formUsername, setFormUsername] = useState(defaultForm.username); const [formAuthMethod, setFormAuthMethod] = useState(defaultForm.authMethod); const [formPrivateKeyPath, setFormPrivateKeyPath] = useState(defaultForm.privateKeyPath); const resetForm = useCallback(() => { setFormName(defaultForm.name); setFormHost(defaultForm.host); setFormPort(defaultForm.port); setFormUsername(defaultForm.username); setFormAuthMethod(defaultForm.authMethod); setFormPrivateKeyPath(defaultForm.privateKeyPath); }, []); const loadProfiles = useCallback(async () => { try { const config = await api.config.get(); // AppConfig type doesn't include ssh field, but ConfigManager returns it at runtime const loaded = config.ssh; setProfiles(loaded?.profiles ?? []); } catch (error) { console.error('[WorkspaceSection] Failed to load profiles:', error); } finally { setLoading(false); } }, []); useEffect(() => { void loadProfiles(); }, [loadProfiles]); // Populate form when editing starts useEffect(() => { if (editingId) { const profile = profiles.find((p) => p.id === editingId); if (profile) { setFormName(profile.name); setFormHost(profile.host); setFormPort(String(profile.port)); setFormUsername(profile.username); setFormAuthMethod(profile.authMethod); setFormPrivateKeyPath(profile.privateKeyPath ?? ''); } } }, [editingId, profiles]); const handleAdd = async (): Promise => { const newProfile: SshConnectionProfile = { id: crypto.randomUUID(), name: formName.trim(), host: formHost.trim(), port: parseInt(formPort, 10) || 22, username: formUsername.trim(), authMethod: formAuthMethod, privateKeyPath: formAuthMethod === 'privateKey' ? formPrivateKeyPath.trim() : undefined, }; await api.config.update('ssh', { profiles: [...profiles, newProfile] }); await loadProfiles(); resetForm(); setShowAddForm(false); void useStore.getState().fetchAvailableContexts(); }; const handleEdit = async (): Promise => { const updatedProfiles = profiles.map((p) => p.id === editingId ? { ...p, name: formName.trim(), host: formHost.trim(), port: parseInt(formPort, 10) || 22, username: formUsername.trim(), authMethod: formAuthMethod, privateKeyPath: formAuthMethod === 'privateKey' ? formPrivateKeyPath.trim() : undefined, } : p ); await api.config.update('ssh', { profiles: updatedProfiles }); await loadProfiles(); setEditingId(null); resetForm(); void useStore.getState().fetchAvailableContexts(); }; const handleDelete = async (id: string): Promise => { const profile = profiles.find((p) => p.id === id); if (!profile) return; const confirmed = await confirm({ title: t('workspaceProfiles.deleteConfirm.title'), message: t('workspaceProfiles.deleteConfirm.message', { name: profile.name }), confirmLabel: t('workspaceProfiles.deleteConfirm.confirmLabel'), variant: 'danger', }); if (!confirmed) return; const filtered = profiles.filter((p) => p.id !== id); await api.config.update('ssh', { profiles: filtered }); await loadProfiles(); void useStore.getState().fetchAvailableContexts(); }; const isFormValid = formName.trim() !== '' && formHost.trim() !== '' && formUsername.trim() !== ''; const renderForm = (onSave: () => Promise, onCancel: () => void): React.JSX.Element => (
setFormName(e.target.value)} placeholder={t('workspaceProfiles.form.namePlaceholder')} className={inputClass} style={inputStyle} />
setFormHost(e.target.value)} placeholder={t('workspaceProfiles.form.hostPlaceholder')} className={inputClass} style={inputStyle} />
setFormPort(e.target.value)} placeholder="22" className={inputClass} style={inputStyle} />
setFormUsername(e.target.value)} placeholder={t('workspaceProfiles.form.usernamePlaceholder')} className={inputClass} style={inputStyle} />
{formAuthMethod === 'privateKey' && (
setFormPrivateKeyPath(e.target.value)} placeholder="~/.ssh/id_rsa" className={inputClass} style={inputStyle} />
)} {formAuthMethod === 'password' && (

{t('workspaceProfiles.form.passwordPrompt')}

)}
); return (

{t('workspaceProfiles.description')}

{loading && (
{t('workspaceProfiles.loading')}
)} {!loading && profiles.length === 0 && !showAddForm && (

{t('workspaceProfiles.empty.title')}

{t('workspaceProfiles.empty.description')}

)} {!loading && (
{profiles.map((profile) => editingId === profile.id ? (
{renderForm(handleEdit, () => { setEditingId(null); resetForm(); })}
) : (

{profile.name}

{profile.username}@{profile.host}:{profile.port}

{profile.authMethod} {t('workspaceProfiles.actions.editProfile')} {t('workspaceProfiles.actions.deleteProfile')}
) )}
)} {!loading && (
{showAddForm ? ( renderForm(handleAdd, () => { setShowAddForm(false); resetForm(); }) ) : ( )}
)}
); };