diff --git a/src/App.jsx b/src/App.jsx index ee72eee8..bd46ed15 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -69,6 +69,7 @@ function App() { const [isEditHostHidden, setIsEditHostHidden] = useState(true); const [currentHostConfig, setCurrentHostConfig] = useState(null); const [isLoggingIn, setIsLoggingIn] = useState(true); + const [isEditing, setIsEditing] = useState(false); useEffect(() => { const handleKeyDown = (e) => { @@ -221,38 +222,56 @@ function App() { }, []); const handleAddHost = () => { - if (addHostForm.ip && addHostForm.user && addHostForm.port && addHostForm.authMethod !== 'Select Auth') { + if (addHostForm.ip && addHostForm.user && addHostForm.port) { + // If not remembering the host, just connect without auth validation + if (!addHostForm.rememberHost) { + connectToHost(); + setIsAddHostHidden(true); + return; + } + + // If remembering the host, validate auth method + if (addHostForm.authMethod === 'Select Auth') { + alert("Please select an authentication method."); + return; + } if (addHostForm.authMethod === 'password' && !addHostForm.password) { setIsNoAuthHidden(false); - } else if (addHostForm.authMethod === 'rsaKey' && !addHostForm.rsaKey) { - setIsNoAuthHidden(false); - } else { - connectToHost(); - if (addHostForm.rememberHost) { - if (!addHostForm.storePassword) { - addHostForm.password = ''; - } - handleSaveHost(); - } + return; } + if (addHostForm.authMethod === 'rsaKey' && !addHostForm.rsaKey) { + setIsNoAuthHidden(false); + return; + } + + // Connect and save if all validation passes + connectToHost(); + if (!addHostForm.storePassword) { + addHostForm.password = ''; + } + handleSaveHost(); + setIsAddHostHidden(true); } else { - alert("Please fill out all fields."); + alert("Please fill out all required fields (IP, User, Port)."); } }; const connectToHost = () => { + // Create a clean host config object + const hostConfig = { + name: addHostForm.name || '', + folder: addHostForm.folder || '', + ip: addHostForm.ip, + user: addHostForm.user, + port: String(addHostForm.port), + password: addHostForm.rememberHost && addHostForm.authMethod === 'password' ? addHostForm.password : undefined, + rsaKey: addHostForm.rememberHost && addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined, + }; + const newTerminal = { id: nextId, - title: addHostForm.name || addHostForm.ip, - hostConfig: { - name: addHostForm.name, - folder: addHostForm.folder, - ip: addHostForm.ip, - user: addHostForm.user, - password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined, - rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined, - port: String(addHostForm.port), - }, + title: hostConfig.name || hostConfig.ip, + hostConfig, terminalRef: null, }; setTerminals([...terminals, newTerminal]); @@ -281,10 +300,31 @@ function App() { }; const connectToHostWithConfig = (hostConfig) => { + // Ensure we have a valid host config + if (!hostConfig || typeof hostConfig !== 'object') { + return; + } + + // Ensure all required fields are present + if (!hostConfig.ip || !hostConfig.user) { + return; + } + + // Create a clean host config object with all required fields + const cleanHostConfig = { + name: hostConfig.name || '', + folder: hostConfig.folder || '', + ip: hostConfig.ip.trim(), + user: hostConfig.user.trim(), + port: hostConfig.port || '22', + password: hostConfig.password?.trim(), + rsaKey: hostConfig.rsaKey?.trim(), + }; + const newTerminal = { id: nextId, - title: hostConfig.name || hostConfig.ip, - hostConfig: hostConfig, + title: cleanHostConfig.name || cleanHostConfig.ip, + hostConfig: cleanHostConfig, terminalRef: null, }; setTerminals([...terminals, newTerminal]); @@ -411,10 +451,21 @@ function App() { const handleEditHost = async (oldConfig, newConfig = null) => { try { if (newConfig) { - await userRef.current.editHost({ - oldHostConfig: oldConfig, - newHostConfig: newConfig, - }); + if (isEditing) return; + setIsEditing(true); + + try { + await userRef.current.editHost({ + oldHostConfig: oldConfig, + newHostConfig: newConfig, + }); + + // Keep the modal visible during the delay + await new Promise(resolve => setTimeout(resolve, 3000)); + } finally { + setIsEditing(false); + setIsEditHostHidden(true); + } return; } @@ -423,6 +474,7 @@ function App() { console.error('Edit failed:', error); setErrorMessage(`Edit failed: ${error}`); setIsErrorHidden(false); + setIsEditing(false); } }; diff --git a/src/apps/Launchpad.jsx b/src/apps/Launchpad.jsx index 03b2774f..90dfcd83 100644 --- a/src/apps/Launchpad.jsx +++ b/src/apps/Launchpad.jsx @@ -161,7 +161,15 @@ function Launchpad({ {activeApp === 'hostViewer' && ( { + if (!hostConfig || typeof hostConfig !== 'object') { + return; + } + if (!hostConfig.ip || !hostConfig.user) { + return; + } + connectToHost(hostConfig); + }} setIsAddHostHidden={setIsAddHostHidden} deleteHost={deleteHost} editHost={editHost} diff --git a/src/apps/ssh/HostViewer.jsx b/src/apps/ssh/HostViewer.jsx index 7efb1ec5..b370e65b 100644 --- a/src/apps/ssh/HostViewer.jsx +++ b/src/apps/ssh/HostViewer.jsx @@ -11,6 +11,7 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e const [draggedHost, setDraggedHost] = useState(null); const [isDraggingOver, setIsDraggingOver] = useState(null); const isMounted = useRef(true); + const [isDeleting, setIsDeleting] = useState(false); const fetchHosts = async () => { try { @@ -153,6 +154,22 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e setDraggedHost(null); }; + const handleDelete = async (e, hostWrapper) => { + e.stopPropagation(); + if (isDeleting) return; + + setIsDeleting(true); + try { + await deleteHost({ _id: hostWrapper._id }); + await new Promise(resolve => setTimeout(resolve, 500)); // Add a small delay for UX + await fetchHosts(); + } catch (error) { + console.error('Failed to delete host:', error); + } finally { + setIsDeleting(false); + } + }; + const renderHostItem = (hostWrapper) => { const hostConfig = hostWrapper.config || {}; @@ -182,27 +199,33 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e className="text-black" onClick={(e) => { e.stopPropagation(); - connectToHost(hostConfig); + if (!hostWrapper.config || !hostWrapper.config.ip || !hostWrapper.config.user) { + return; + } + connectToHost(hostWrapper.config); }} + disabled={isDeleting} sx={{ backgroundColor: "#6e6e6e", - "&:hover": { backgroundColor: "#0f0f0f" } + "&:hover": { backgroundColor: "#0f0f0f" }, + opacity: isDeleting ? 0.5 : 1, + cursor: isDeleting ? "not-allowed" : "pointer" }} > Connect diff --git a/src/modals/EditHostModal.jsx b/src/modals/EditHostModal.jsx index 411dbe98..21f87dca 100644 --- a/src/modals/EditHostModal.jsx +++ b/src/modals/EditHostModal.jsx @@ -27,22 +27,24 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => { const [showPassword, setShowPassword] = useState(false); const [activeTab, setActiveTab] = useState(0); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { - if (hostConfig && !isHidden) { + if (!isHidden && hostConfig) { setForm({ - name: hostConfig.name || "", - folder: hostConfig.folder || "", - ip: hostConfig.ip || "", - user: hostConfig.user || "", - password: hostConfig.password || "", + name: hostConfig.name || '', + folder: hostConfig.folder || '', + ip: hostConfig.ip || '', + user: hostConfig.user || '', + password: hostConfig.password || '', + rsaKey: hostConfig.rsaKey || '', port: hostConfig.port || 22, - authMethod: hostConfig.password ? "password" : hostConfig.rsaKey ? "rsaKey" : "Select Auth", + authMethod: hostConfig.password ? 'password' : hostConfig.rsaKey ? 'rsaKey' : 'Select Auth', rememberHost: true, storePassword: true, }); } - }, [hostConfig, isHidden]); + }, [isHidden, hostConfig]); const handleFileChange = (e) => { const file = e.target.files[0]; @@ -68,7 +70,9 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH setForm((prev) => ({ ...prev, storePassword: Boolean(checked), - authMethod: checked ? 'password' : 'Select Auth' + password: checked ? prev.password : "", + rsaKey: checked ? prev.rsaKey : "", + authMethod: checked ? prev.authMethod : "Select Auth" })); }; @@ -85,22 +89,23 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH return true; }; - const handleSave = async (e) => { - e.preventDefault(); + const handleSubmit = async (event) => { + event.preventDefault(); + if (isLoading) return; + + setIsLoading(true); try { - const newConfig = { - ...form, + await handleEditHost(hostConfig, { + name: form.name || form.ip, + folder: form.folder, + ip: form.ip, + user: form.user, + password: form.authMethod === 'password' ? form.password : undefined, + rsaKey: form.authMethod === 'rsaKey' ? form.rsaKey : undefined, port: String(form.port), - }; - - if (form.authMethod === 'rsaKey' || !form.storePassword) { - newConfig.password = ''; - } - - await handleEditHost(hostConfig, newConfig); - setIsEditHostHidden(true); - } catch (error) { - console.error('Failed to save:', error); + }); + } finally { + setIsLoading(false); } }; @@ -108,15 +113,20 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH setIsEditHostHidden(true)} + onClose={() => !isLoading && setIsEditHostHidden(true)} sx={{ + position: 'fixed', + inset: 0, display: 'flex', - justifyContent: 'center', alignItems: 'center', + justifyContent: 'center', + backdropFilter: 'blur(5px)', + backgroundColor: 'rgba(0, 0, 0, 0.2)', }} > Edit Host -
+ setActiveTab(val)} @@ -270,7 +280,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH > - + )} @@ -304,7 +314,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH {form.authMethod === 'rsaKey' && form.storePassword && ( - RSA Key + Public Key
diff --git a/src/modals/NoAuthenticationModal.jsx b/src/modals/NoAuthenticationModal.jsx index 24c753f5..a08feede 100644 --- a/src/modals/NoAuthenticationModal.jsx +++ b/src/modals/NoAuthenticationModal.jsx @@ -15,15 +15,25 @@ import { Option, } from '@mui/joy'; import theme from '/src/theme'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, handleAuthSubmit }) => { const [showPassword, setShowPassword] = useState(false); + // Initialize form with default values if not provided + useEffect(() => { + if (!form.authMethod) { + setForm(prev => ({ + ...prev, + authMethod: 'Select Auth' + })); + } + }, []); + const isFormValid = () => { - if (form.authMethod === 'Select Auth') return false; + if (!form.authMethod || form.authMethod === 'Select Auth') return false; if (form.authMethod === 'rsaKey' && !form.rsaKey) return false; if (form.authMethod === 'password' && !form.password) return false; return true; @@ -46,7 +56,11 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han setIsNoAuthHidden(true); } }} - disableBackdropClic + sx={{ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }} > - Authentication Required + Authentication Required
- + - Authentication Method + Authentication Method + {form.authMethod === 'password' && ( - Password + Password
setForm({ ...form, password: e.target.value })} - required + value={form.password || ''} + onChange={(e) => setForm(prev => ({ ...prev, password: e.target.value }))} sx={{ backgroundColor: theme.palette.general.primary, color: theme.palette.text.primary, - flex: 1, + flex: 1 }} /> setShowPassword(!showPassword)} sx={{ color: theme.palette.text.primary, - marginLeft: 1, + marginLeft: 1 }} > {showPassword ? : } @@ -115,34 +124,44 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han
)} + {form.authMethod === 'rsaKey' && ( - RSA Key - { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (event) => { - setForm({ ...form, rsaKey: event.target.result }); - }; - reader.readAsText(file); - } - }} - required + Public Key + )} +