/** * 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 { 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 authMethodOptions: readonly { value: SshAuthMethod; label: string }[] = [ { value: 'auto', label: 'Auto (from SSH Config)' }, { value: 'agent', label: 'SSH Agent' }, { value: 'privateKey', label: 'Private Key' }, { value: 'password', label: 'Password' }, ]; const defaultForm = { name: '', host: '', port: '22', username: '', authMethod: 'auto' as SshAuthMethod, privateKeyPath: '', }; export const WorkspaceSection = (): React.JSX.Element => { const [profiles, setProfiles] = useState([]); const [loading, setLoading] = useState(true); const [editingId, setEditingId] = useState(null); const [showAddForm, setShowAddForm] = useState(false); // 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: 'Delete Profile', message: `Are you sure you want to delete "${profile.name}"? This cannot be undone.`, confirmLabel: 'Delete', 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="My Server" className={inputClass} style={inputStyle} />
setFormHost(e.target.value)} placeholder="hostname or IP" className={inputClass} style={inputStyle} />
setFormPort(e.target.value)} placeholder="22" className={inputClass} style={inputStyle} />
setFormUsername(e.target.value)} placeholder="user" className={inputClass} style={inputStyle} />
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- SettingsSelect is a custom dropdown without a native control */}
{formAuthMethod === 'privateKey' && (
setFormPrivateKeyPath(e.target.value)} placeholder="~/.ssh/id_rsa" className={inputClass} style={inputStyle} />
)} {formAuthMethod === 'password' && (

You will be prompted for the password when connecting.

)}
); return (

Save SSH connection profiles for quick reconnection

{loading && (
Loading profiles...
)} {!loading && profiles.length === 0 && !showAddForm && (

No saved profiles

Add an SSH profile to connect quickly

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

{profile.name}

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

{profile.authMethod} Edit profile Delete profile
) )}
)} {!loading && (
{showAddForm ? ( renderForm(handleAdd, () => { setShowAddForm(false); resetForm(); }) ) : ( )}
)}
); };