From 073e9a969b4d56b4b44b5ebd4f4a5d97dff1a507 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sat, 26 Jul 2025 20:07:25 -0500 Subject: [PATCH] Update ssh sidebar to follow new scheme --- src/apps/SSH/Terminal/SSHSidebar.tsx | 1512 ++------------------------ src/components/ui/resizable.tsx | 4 +- 2 files changed, 65 insertions(+), 1451 deletions(-) diff --git a/src/apps/SSH/Terminal/SSHSidebar.tsx b/src/apps/SSH/Terminal/SSHSidebar.tsx index 93863678..173bb686 100644 --- a/src/apps/SSH/Terminal/SSHSidebar.tsx +++ b/src/apps/SSH/Terminal/SSHSidebar.tsx @@ -1,10 +1,7 @@ -import React, { useState, useMemo } from 'react'; -import { useForm, Controller } from "react-hook-form"; +import React, { useState } from 'react'; import { CornerDownLeft, - Plus, - MoreVertical, Hammer } from "lucide-react" @@ -27,26 +24,11 @@ import { } from "@/components/ui/separator.tsx" import { Sheet, - SheetClose, SheetContent, - SheetDescription, - SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet.tsx"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs.tsx"; -import { Switch } from "@/components/ui/switch.tsx"; -import axios from "axios"; import { Accordion, AccordionContent, @@ -54,229 +36,57 @@ import { AccordionTrigger, } from "@/components/ui/accordion.tsx"; import { ScrollArea } from "@/components/ui/scroll-area.tsx"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { getSSHHosts } from "@/apps/SSH/ssh-axios"; + +interface SSHHost { + id: number; + name: string; + ip: string; + port: number; + username: string; + folder: string; + tags: string[]; + pin: boolean; + authType: string; + password?: string; + key?: string; + keyPassword?: string; + keyType?: string; + enableTerminal: boolean; + enableTunnel: boolean; + enableConfigEditor: boolean; + defaultPath: string; + tunnelConnections: any[]; + createdAt: string; + updatedAt: string; +} interface SidebarProps { onSelectView: (view: string) => void; - onAddHostSubmit: (data: any) => void; onHostConnect: (hostConfig: any) => void; allTabs: { id: number; title: string; terminalRef: React.RefObject }[]; runCommandOnTabs: (tabIds: number[], command: string) => void; } -interface AuthPromptFormData { - password: string; - authMethod: string; - sshKeyFile: File | null; - sshKeyContent?: string; - keyPassword?: string; - keyType?: string; -} - -interface AddHostFormData { - name: string; - folder: string; - tags: string[]; - tagsInput?: string; - ip: string; - port: number; - username: string; - password: string; - authMethod: string; - sshKeyFile: File | null; - sshKeyContent?: string; - keyPassword?: string; - keyType?: string; - saveAuthMethod: boolean; - isPinned: boolean; -} - -export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect, allTabs, runCommandOnTabs }: SidebarProps): React.ReactElement { - const addHostForm = useForm({ - defaultValues: { - name: '', - folder: '', - tags: [], - tagsInput: '', - ip: '', - port: 22, - username: '', - password: '', - authMethod: 'password', - sshKeyFile: null, - saveAuthMethod: true, - isPinned: false - } - }) - - const [folders, setFolders] = useState([]); - const [foldersLoading, setFoldersLoading] = useState(false); - const [foldersError, setFoldersError] = useState(null); - React.useEffect(() => { - async function fetchFolders() { - setFoldersLoading(true); - setFoldersError(null); - try { - const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1]; - const res = await axios.get( - (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + '/ssh/folders', - { headers: { Authorization: `Bearer ${jwt}` } } - ); - setFolders(res.data || []); - } catch (err: any) { - setFoldersError('Failed to load folders'); - } finally { - setFoldersLoading(false); - } - } - fetchFolders(); - }, []); - - const folderValue = addHostForm.watch('folder'); - const filteredFolders = React.useMemo(() => { - if (!folderValue) return folders; - return folders.filter(f => f.toLowerCase().includes(folderValue.toLowerCase())); - }, [folderValue, folders]); - - const tags = addHostForm.watch('tags') || []; - const tagsInput = addHostForm.watch('tagsInput') || ''; - - const handleTagsInputChange = (e: React.ChangeEvent) => { - const value = e.target.value; - if (value.endsWith(' ')) { - const tag = value.trim(); - if (tag && !tags.includes(tag)) { - addHostForm.setValue('tags', [...tags, tag]); - } - addHostForm.setValue('tagsInput', ''); - } else { - addHostForm.setValue('tagsInput', value); - } - }; - - const handleRemoveTag = (tag: string) => { - addHostForm.setValue('tags', tags.filter((t) => t !== tag)); - }; - - const [folderDropdownOpen, setFolderDropdownOpen] = useState(false); - const [editFolderDropdownOpen, setEditFolderDropdownOpen] = useState(false); - const folderInputRef = React.useRef(null); - const folderDropdownRef = React.useRef(null); - const editFolderInputRef = React.useRef(null); - const editFolderDropdownRef = React.useRef(null); - - React.useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - folderDropdownRef.current && - !folderDropdownRef.current.contains(event.target as Node) && - folderInputRef.current && - !folderInputRef.current.contains(event.target as Node) - ) { - setFolderDropdownOpen(false); - } - if ( - editFolderDropdownRef.current && - !editFolderDropdownRef.current.contains(event.target as Node) && - editFolderInputRef.current && - !editFolderInputRef.current.contains(event.target as Node) - ) { - setEditFolderDropdownOpen(false); - } - } - if (folderDropdownOpen || editFolderDropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - } else { - document.removeEventListener('mousedown', handleClickOutside); - } - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [folderDropdownOpen, editFolderDropdownOpen]); - - const [submitting, setSubmitting] = useState(false); - const [submitError, setSubmitError] = useState(null); - const [sheetOpen, setSheetOpen] = useState(false); - - React.useEffect(() => { - if (!sheetOpen) { - setFolderDropdownOpen(false); - } - }, [sheetOpen]); - - React.useEffect(() => { - if (!sheetOpen) { - setSubmitError(null); - } - }, [sheetOpen]); - - const onAddHostSubmitReset = async (data: AddHostFormData) => { - setSubmitting(true); - setSubmitError(null); - try { - let sshKeyContent = data.sshKeyContent; - if (data.sshKeyFile instanceof File) { - sshKeyContent = await data.sshKeyFile.text(); - } - const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1]; - await axios.post( - (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + '/ssh/host', - { - name: data.name, - folder: data.folder, - tags: data.tags, - ip: data.ip, - port: data.port, - username: data.username, - password: data.password, - authMethod: data.authMethod, - key: sshKeyContent, - keyPassword: data.keyPassword, - keyType: data.keyType === 'auto' ? '' : data.keyType, - saveAuthMethod: data.saveAuthMethod, - isPinned: data.isPinned - }, - { headers: { Authorization: `Bearer ${jwt}` } } - ); - setSheetOpen(false); - addHostForm.reset(); - if (data.folder && !folders.includes(data.folder)) { - setFolders(prev => [...prev, data.folder]); - } - } catch (err: any) { - setSubmitError(err?.response?.data?.error || 'Failed to add SSH host'); - } finally { - setSubmitting(false); - } - }; - - const handleFolderClick = (folder: string) => { - addHostForm.setValue('folder', folder); - setFolderDropdownOpen(false); - }; - - const [hosts, setHosts] = useState([]); +export function SSHSidebar({ onSelectView, onHostConnect, allTabs, runCommandOnTabs }: SidebarProps): React.ReactElement { + const [hosts, setHosts] = useState([]); const [hostsLoading, setHostsLoading] = useState(false); const [hostsError, setHostsError] = useState(null); - const prevHostsRef = React.useRef([]); + const prevHostsRef = React.useRef([]); + const fetchHosts = React.useCallback(async () => { setHostsLoading(true); setHostsError(null); try { - const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1]; - const res = await axios.get( - (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + '/ssh/host', - { headers: { Authorization: `Bearer ${jwt}` } } - ); - const newHosts = res.data || []; + const newHosts = await getSSHHosts(); + // Filter hosts to only show those with enableTerminal: true + const terminalHosts = newHosts.filter(host => host.enableTerminal); + const prevHosts = prevHostsRef.current; const isSame = - newHosts.length === prevHosts.length && - newHosts.every((h: any, i: number) => { + terminalHosts.length === prevHosts.length && + terminalHosts.every((h: SSHHost, i: number) => { const prev = prevHosts[i]; if (!prev) return false; return ( @@ -287,16 +97,15 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect, allTa h.port === prev.port && h.username === prev.username && h.password === prev.password && - h.authMethod === prev.authMethod && + h.authType === prev.authType && h.key === prev.key && - h.saveAuthMethod === prev.saveAuthMethod && - h.isPinned === prev.isPinned && - (Array.isArray(h.tags) ? h.tags.join(',') : h.tags) === (Array.isArray(prev.tags) ? prev.tags.join(',') : prev.tags) + h.pin === prev.pin && + JSON.stringify(h.tags) === JSON.stringify(prev.tags) ); }); if (!isSame) { - setHosts(newHosts); - prevHostsRef.current = newHosts; + setHosts(terminalHosts); + prevHostsRef.current = terminalHosts; } } catch (err: any) { setHostsError('Failed to load hosts'); @@ -304,16 +113,12 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect, allTa setHostsLoading(false); } }, []); + React.useEffect(() => { fetchHosts(); const interval = setInterval(fetchHosts, 10000); return () => clearInterval(interval); }, [fetchHosts]); - React.useEffect(() => { - if (!submitting && !sheetOpen) { - fetchHosts(); - } - }, [submitting, sheetOpen, fetchHosts]); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); @@ -328,13 +133,13 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect, allTa return hosts.filter(h => { const name = (h.name || "").toLowerCase(); const ip = (h.ip || "").toLowerCase(); - const tags = Array.isArray(h.tags) ? h.tags : (typeof h.tags === 'string' ? h.tags.split(',').map((t: string) => t.trim().toLowerCase()) : []); - return name.includes(q) || ip.includes(q) || tags.some((tag: string) => tag.includes(q)); + const tags = Array.isArray(h.tags) ? h.tags : []; + return name.includes(q) || ip.includes(q) || tags.some((tag: string) => tag.toLowerCase().includes(q)); }); }, [hosts, debouncedSearch]); const hostsByFolder = React.useMemo(() => { - const map: Record = {}; + const map: Record = {}; filteredHosts.forEach(h => { const folder = h.folder && h.folder.trim() ? h.folder : 'No Folder'; if (!map[folder]) map[folder] = []; @@ -342,6 +147,7 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect, allTa }); return map; }, [filteredHosts]); + const sortedFolders = React.useMemo(() => { const folders = Object.keys(hostsByFolder); folders.sort((a, b) => { @@ -351,274 +157,22 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect, allTa }); return folders; }, [hostsByFolder]); - const getSortedHosts = (arr: any[]) => { - const pinned = arr.filter(h => h.isPinned).sort((a, b) => (a.name || '').localeCompare(b.name || '')); - const rest = arr.filter(h => !h.isPinned).sort((a, b) => (a.name || '').localeCompare(b.name || '')); + + const getSortedHosts = (arr: SSHHost[]) => { + const pinned = arr.filter(h => h.pin).sort((a, b) => (a.name || '').localeCompare(b.name || '')); + const rest = arr.filter(h => !h.pin).sort((a, b) => (a.name || '').localeCompare(b.name || '')); return [...pinned, ...rest]; }; - const [editHostOpen, setEditHostOpen] = useState(false); - - React.useEffect(() => { - if (!editHostOpen) { - setEditFolderDropdownOpen(false); - } - }, [editHostOpen]); - - const [editHostData, setEditHostData] = useState(null); - const editHostForm = useForm({ - defaultValues: { - name: '', - folder: '', - tags: [], - tagsInput: '', - ip: '', - port: 22, - username: '', - password: '', - authMethod: 'password', - sshKeyFile: null, - saveAuthMethod: false, - isPinned: false - } - }); - React.useEffect(() => { - if (editHostData) { - editHostForm.reset({ - ...editHostData, - tags: editHostData.tags ? (Array.isArray(editHostData.tags) ? editHostData.tags : (typeof editHostData.tags === 'string' ? editHostData.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : [])) : [], - tagsInput: '', - sshKeyFile: null, - sshKeyContent: editHostData.key || '', - keyPassword: editHostData.keyPassword || '', - keyType: editHostData.keyType || '', - }); - } - }, [editHostData]); - - const editTags = editHostForm.watch('tags') || []; - const editTagsInput = editHostForm.watch('tagsInput') || ''; - - const handleEditTagsInputChange = (e: React.ChangeEvent) => { - const value = e.target.value; - if (value.endsWith(' ')) { - const tag = value.trim(); - if (tag && !editTags.includes(tag)) { - editHostForm.setValue('tags', [...editTags, tag]); - } - editHostForm.setValue('tagsInput', ''); - } else { - editHostForm.setValue('tagsInput', value); - } - }; - - const handleRemoveEditTag = (tag: string) => { - editHostForm.setValue('tags', editTags.filter((t) => t !== tag)); - }; - const onEditHostSubmit = async (data: AddHostFormData) => { - let sshKeyContent = data.sshKeyContent; - if (data.sshKeyFile instanceof File) { - sshKeyContent = await data.sshKeyFile.text(); - } - try { - const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1]; - if (!editHostData?.id) { - throw new Error('No host ID found for editing'); - } - const response = await axios.put( - (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + `/ssh/host/${editHostData.id}`, - { - name: data.name, - folder: data.folder, - tags: data.tags, - ip: data.ip, - port: data.port, - username: data.username, - password: data.password, // always send - authMethod: data.authMethod, - key: sshKeyContent, // always send - keyPassword: data.keyPassword, // always send - keyType: data.keyType, // always send - saveAuthMethod: data.saveAuthMethod, - isPinned: data.isPinned - }, - { headers: { Authorization: `Bearer ${jwt}` } } - ); - setEditHostOpen(false); - fetchHosts(); - } catch (err: any) { - } - }; - const onDeleteHost = async (host: any) => { - try { - const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1]; - await axios.delete( - (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + `/ssh/host/${host.id}`, - { headers: { Authorization: `Bearer ${jwt}` } } - ); - fetchHosts(); - } catch (err) { - } - }; - - const [hostPopoverOpen, setHostPopoverOpen] = useState>({}); - const handlePopoverOpenChange = (hostId: string, open: boolean) => { - setHostPopoverOpen(prev => ({ ...prev, [hostId]: open })); - }; - - const [authPromptOpen, setAuthPromptOpen] = useState(false); - const [authPromptHost, setAuthPromptHost] = useState(null); - const authPromptForm = useForm({ - defaultValues: { - password: '', - authMethod: 'password', - sshKeyFile: null, - } - }); - - const handleHostConnect = (host: any) => { - const hasSavedAuth = host.saveAuthMethod && ((host.authMethod === 'password' && host.password) || (host.authMethod === 'key' && host.key)); - - const hasUsername = host.username && host.username.trim() !== ''; - - if (hasSavedAuth && hasUsername) { - onHostConnect(host); - } else { - setAuthPromptHost(host); - setAuthPromptOpen(true); - } - }; - - const onAuthPromptSubmit = async (data: AuthPromptFormData) => { - let sshKeyContent = data.sshKeyContent; - if (data.sshKeyFile instanceof File) { - sshKeyContent = await data.sshKeyFile.text(); - } - - const hostConfig = { - ...authPromptHost, - password: data.authMethod === 'password' ? data.password : undefined, - key: data.authMethod === 'key' ? sshKeyContent : undefined, - keyPassword: data.authMethod === 'key' ? data.keyPassword : undefined, - keyType: data.authMethod === 'key' ? (data.keyType === 'auto' ? undefined : data.keyType) : undefined, - authMethod: data.authMethod, - }; - - if (!hostConfig.username || !hostConfig.ip || !hostConfig.port) { - return; - } - - setAuthPromptOpen(false); - onHostConnect(hostConfig); - }; - - React.useEffect(() => { - if (!authPromptOpen) { - setTimeout(() => { - authPromptForm.reset(); - setAuthPromptHost(null); - }, 100); - } else { - } - }, [authPromptOpen, authPromptForm]); - - // Key type options - const keyTypeOptions = [ - { value: 'auto', label: 'Auto-detect' }, - { value: 'ssh-rsa', label: 'RSA' }, - { value: 'ssh-ed25519', label: 'ED25519' }, - { value: 'ecdsa-sha2-nistp256', label: 'ECDSA NIST P-256' }, - { value: 'ecdsa-sha2-nistp384', label: 'ECDSA NIST P-384' }, - { value: 'ecdsa-sha2-nistp521', label: 'ECDSA NIST P-521' }, - { value: 'ssh-dss', label: 'DSA' }, - { value: 'ssh-rsa-sha2-256', label: 'RSA SHA2-256' }, - { value: 'ssh-rsa-sha2-512', label: 'RSA SHA2-512' }, - ]; - - const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); - const [editKeyTypeDropdownOpen, setEditKeyTypeDropdownOpen] = useState(false); - const keyTypeDropdownRef = React.useRef(null); - const editKeyTypeDropdownRef = React.useRef(null); - const keyTypeButtonRef = React.useRef(null); - const editKeyTypeButtonRef = React.useRef(null); - - // Close dropdown on outside click (add form) - React.useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - keyTypeDropdownRef.current && - !keyTypeDropdownRef.current.contains(event.target as Node) && - keyTypeButtonRef.current && - !keyTypeButtonRef.current.contains(event.target as Node) - ) { - setKeyTypeDropdownOpen(false); - } - } - if (keyTypeDropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - } else { - document.removeEventListener('mousedown', handleClickOutside); - } - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [keyTypeDropdownOpen]); - // Close dropdown on outside click (edit form) - React.useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - editKeyTypeDropdownRef.current && - !editKeyTypeDropdownRef.current.contains(event.target as Node) && - editKeyTypeButtonRef.current && - !editKeyTypeButtonRef.current.contains(event.target as Node) - ) { - setEditKeyTypeDropdownOpen(false); - } - } - if (editKeyTypeDropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - } else { - document.removeEventListener('mousedown', handleClickOutside); - } - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [editKeyTypeDropdownOpen]); - - const [keyTypeDropdownOpenAuth, setKeyTypeDropdownOpenAuth] = useState(false); - const keyTypeDropdownAuthRef = React.useRef(null); - const keyTypeButtonAuthRef = React.useRef(null); - - // Close dropdown on outside click (auth prompt) - React.useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - keyTypeDropdownAuthRef.current && - !keyTypeDropdownAuthRef.current.contains(event.target as Node) && - keyTypeButtonAuthRef.current && - !keyTypeButtonAuthRef.current.contains(event.target as Node) - ) { - setKeyTypeDropdownOpenAuth(false); - } - } - if (keyTypeDropdownOpenAuth) { - document.addEventListener('mousedown', handleClickOutside); - } else { - document.removeEventListener('mousedown', handleClickOutside); - } - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [keyTypeDropdownOpenAuth]); - // Tools Sheet State const [toolsSheetOpen, setToolsSheetOpen] = useState(false); const [toolsCommand, setToolsCommand] = useState(""); const [selectedTabIds, setSelectedTabIds] = useState([]); + const handleTabToggle = (tabId: number) => { setSelectedTabIds(prev => prev.includes(tabId) ? prev.filter(id => id !== tabId) : [...prev, tabId]); }; - // --- Fix: Run Command logic --- + const handleRunCommand = () => { if (selectedTabIds.length && toolsCommand.trim()) { // Ensure command ends with newline @@ -653,404 +207,6 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect, allTa - - { if (!submitting) setSheetOpen(open); }}> - - - - - - Add Host - - Add a new SSH host connection with authentication details. - - - -
- {submitError && ( -
{submitError}
- )} -
- - {/* Name */} - ( - - Name - - - - - - )} - /> - - {/* Folder */} - ( - - Folder - - { - if (typeof field.ref === 'function') field.ref(el); - (folderInputRef as React.MutableRefObject).current = el; - }} - placeholder="e.g. Work" - autoComplete="off" - value={field.value} - onFocus={() => setFolderDropdownOpen(true)} - onChange={e => { - field.onChange(e); - setFolderDropdownOpen(true); - }} - disabled={foldersLoading} - /> - - {/* Folder dropdown menu */} - {folderDropdownOpen && filteredFolders.length > 0 && ( -
-
- {filteredFolders.map((folder) => ( - - ))} -
-
- )} - {foldersLoading &&
Loading folders...
} - {foldersError &&
{foldersError}
} - -
- )} - /> - - {/* Tags */} - ( - - Tags - - - - {/* Tag chips */} - {tags.length > 0 && ( -
- {tags.map((tag) => ( - - ))} -
- )} - -
- )} - /> - - Connection Details - - - ( - - IP - - - - - - )} - /> - - ( - - Username - - - - - - )} - /> - - ( - - Port - - field.onChange(Number(e.target.value) || 22)} - /> - - - - )} - /> - - Authentication - - - ( - - - Password - SSH Key - - - - ( - - Password - - - - - - )} - /> - - - - ( - - SSH Private Key - -
- { - const file = e.target.files?.[0]; - field.onChange(file || null); - }} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" - /> - -
-
-
- )} - /> - ( - - Key Password (if protected) - - - - - - )} - /> - ( - - Key Type - -
- - {keyTypeDropdownOpen && ( -
-
- {keyTypeOptions.map(opt => ( - - ))} -
-
- )} -
-
- -
- )} - /> -
-
- )} - /> - - ( - - -
- - Save Auth Method -
-
- -
- )} - /> - - Other - - ( - - -
- - Pin Connection -
-
- -
- )} - /> - - -
- - - - - - - - - - - - - -
-
-
-
{/* Search bar */} @@ -1089,14 +245,7 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect, allTa
{ - setEditHostData(host); - setEditHostOpen(true); - }} - popoverOpen={!!hostPopoverOpen[host.id]} - setPopoverOpen={(open: boolean) => handlePopoverOpenChange(host.id, open)} + onHostConnect={onHostConnect} />
))} @@ -1178,564 +327,29 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect, allTa - {/* Edit Host Sheet */} - { - if (!open) { - setTimeout(() => { - setEditHostData(null); - editHostForm.reset(); - }, 100); - } - setEditHostOpen(open); - }}> - - - Edit Host - - Modify the SSH host connection settings and authentication details. - - -
-
- - ( - - Name - - - - - - )} - /> - ( - - Folder - - { - if (typeof field.ref === 'function') field.ref(el); - (editFolderInputRef as React.MutableRefObject).current = el; - }} - onFocus={() => setEditFolderDropdownOpen(true)} - onChange={e => { - field.onChange(e); - setEditFolderDropdownOpen(true); - }} - disabled={foldersLoading} - /> - - {editFolderDropdownOpen && filteredFolders.length > 0 && ( -
-
- {filteredFolders.map((folder) => ( - - ))} -
-
- )} - {foldersLoading &&
Loading folders...
} - {foldersError &&
{foldersError}
} - -
- )} - /> - ( - - Tags - - - - {/* Tag chips */} - {editTags.length > 0 && ( -
- {editTags.map((tag) => ( - - ))} -
- )} - -
- )} - /> - Connection Details - - ( - - IP - - - - - - )} - /> - ( - - Username - - - - - - )} - /> - ( - - Port - - field.onChange(Number(e.target.value) || 22)} - /> - - - - )} - /> - Authentication - - ( - - - Password - SSH Key - - - ( - - Password - - - - - - )} - /> - - - ( - - SSH Private Key - -
- { - const file = e.target.files?.[0]; - field.onChange(file || null); - }} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" - /> - -
-
-
- )} - /> - ( - - Key Password (if protected) - - - - - - )} - /> - ( - - Key Type - -
- - {editKeyTypeDropdownOpen && ( -
-
- {keyTypeOptions.map(opt => ( - - ))} -
-
- )} -
-
- -
- )} - /> -
-
- )} - /> - ( - - -
- - Save Auth Method -
-
- -
- )} - /> - Other - - ( - - -
- - Pin Connection -
-
- -
- )} - /> - - -
- - - - - - - - - -
-
- {/* Auth Prompt Sheet */} - { - setAuthPromptOpen(open); - }}> - - - Enter Credentials - - Provide authentication credentials to connect to the SSH host. - - -
-
- - ( - - - Password - SSH Key - - - - ( - - Password - - - - - - )} - /> - - - - ( - - SSH Private Key - -
- { - const file = e.target.files?.[0]; - field.onChange(file || null); - }} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" - /> - -
-
-
- )} - /> - ( - - Key Password (if protected) - - - - - - )} - /> - ( - - Key Type - -
- - {keyTypeDropdownOpenAuth && ( -
-
- {keyTypeOptions.map(opt => ( - - ))} -
-
- )} -
-
- -
- )} - /> -
-
- )} - /> - - -
- - - - - - - - - -
-
); } -const HostMenuItem = React.memo(function HostMenuItem({ host, onHostConnect, onDeleteHost, onEditHost, popoverOpen, setPopoverOpen }: any) { - const tags = Array.isArray(host.tags) ? host.tags : (typeof host.tags === 'string' ? host.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : []); +const HostMenuItem = React.memo(function HostMenuItem({ host, onHostConnect }: { host: SSHHost; onHostConnect: (hostConfig: any) => void }) { + const tags = Array.isArray(host.tags) ? host.tags : []; const hasTags = tags.length > 0; return ( -
-
+
+
- {/* Left: Name + Star - Horizontal scroll only */} -
onHostConnect(host)} > -
- {host.isPinned && } - {host.name || host.ip} +
+ {host.pin && } + {host.name || host.ip}
-
- - - - - - - - - -
{hasTags && ( -
+
{tags.map((tag: string) => ( {tag} diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx index fe0ebbde..dd5a6bad 100644 --- a/src/components/ui/resizable.tsx +++ b/src/components/ui/resizable.tsx @@ -37,13 +37,13 @@ function ResizableHandle({ div]:rotate-90", + "relative flex w-1 items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-1 data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90 bg-[#434345] hover:bg-[#2a2a2c] active:bg-[#1a1a1c] transition-colors duration-150", className )} {...props} > {withHandle && ( -
+
)}