From fda8c7ce4bbc31ff1feccc55d75268d1bc623436 Mon Sep 17 00:00:00 2001 From: Karmaa Date: Sun, 16 Mar 2025 01:51:14 -0500 Subject: [PATCH] Waits for user to be able to log in. Improved UI for profile, edit and add host, and added organizational features to the host app (Folders, search, etc.) --- src/App.jsx | 518 ++++++++++++++++++++++------------- src/apps/Launchpad.jsx | 32 +-- src/apps/ssh/HostViewer.jsx | 269 ++++++++++++++---- src/backend/database.cjs | 13 +- src/modals/AddHostModal.jsx | 389 ++++++++++++++++---------- src/modals/EditHostModal.jsx | 465 ++++++++++++++++++------------- src/modals/ProfileModal.jsx | 168 +++++------- 7 files changed, 1162 insertions(+), 692 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index a2992c0c..ee72eee8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -31,6 +31,7 @@ function App() { const [nextId, setNextId] = useState(1); const [addHostForm, setAddHostForm] = useState({ name: "", + folder: "", ip: "", user: "", password: "", @@ -41,6 +42,7 @@ function App() { }); const [editHostForm, setEditHostForm] = useState({ name: "", + folder: "", ip: "", user: "", password: "", @@ -66,6 +68,7 @@ function App() { const [splitTabIds, setSplitTabIds] = useState([]); const [isEditHostHidden, setIsEditHostHidden] = useState(true); const [currentHostConfig, setCurrentHostConfig] = useState(null); + const [isLoggingIn, setIsLoggingIn] = useState(true); useEffect(() => { const handleKeyDown = (e) => { @@ -124,23 +127,97 @@ function App() { useEffect(() => { const sessionToken = localStorage.getItem('sessionToken'); - if (sessionToken) { - setTimeout(() => { - handleLoginUser({ + let isComponentMounted = true; + let isLoginInProgress = false; + + if (userRef.current?.getUser()) { + setIsLoggingIn(false); + setIsLoginUserHidden(true); + return; + } + + if (!sessionToken) { + setIsLoggingIn(false); + setIsLoginUserHidden(false); + return; + } + + setIsLoggingIn(true); + let loginAttempts = 0; + const maxAttempts = 50; + let attemptLoginInterval; + + const loginTimeout = setTimeout(() => { + if (isComponentMounted) { + clearInterval(attemptLoginInterval); + if (!userRef.current?.getUser()) { + localStorage.removeItem('sessionToken'); + setIsLoginUserHidden(false); + setIsLoggingIn(false); + setErrorMessage('Login timed out. Please try again.'); + setIsErrorHidden(false); + } + } + }, 10000); + + const attemptLogin = () => { + if (!isComponentMounted || isLoginInProgress) return; + + if (loginAttempts >= maxAttempts || userRef.current?.getUser()) { + clearTimeout(loginTimeout); + clearInterval(attemptLoginInterval); + + if (!userRef.current?.getUser()) { + localStorage.removeItem('sessionToken'); + setIsLoginUserHidden(false); + setIsLoggingIn(false); + setErrorMessage('Login timed out. Please try again.'); + setIsErrorHidden(false); + } + return; + } + + if (userRef.current) { + isLoginInProgress = true; + userRef.current.loginUser({ sessionToken, onSuccess: () => { - setIsLoginUserHidden(true); + if (isComponentMounted) { + clearTimeout(loginTimeout); + clearInterval(attemptLoginInterval); + setIsLoginUserHidden(true); + setIsLoggingIn(false); + setIsErrorHidden(true); + } + isLoginInProgress = false; }, onFailure: (error) => { - setErrorMessage(`Auto-login failed: ${error}`); - setIsErrorHidden(false); - setIsLoginUserHidden(false); + if (isComponentMounted) { + if (!userRef.current?.getUser()) { + clearTimeout(loginTimeout); + clearInterval(attemptLoginInterval); + localStorage.removeItem('sessionToken'); + setErrorMessage(`Auto-login failed: ${error}`); + setIsErrorHidden(false); + setIsLoginUserHidden(false); + setIsLoggingIn(false); + } + } + isLoginInProgress = false; }, }); - }, 500); - } else { - setIsLoginUserHidden(false); - } + } + loginAttempts++; + }; + + attemptLoginInterval = setInterval(attemptLogin, 100); + attemptLogin(); + + return () => { + isComponentMounted = false; + clearTimeout(loginTimeout); + clearInterval(attemptLoginInterval); + }; }, []); const handleAddHost = () => { @@ -168,6 +245,8 @@ function App() { 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, @@ -180,7 +259,7 @@ function App() { setActiveTab(nextId); setNextId(nextId + 1); setIsAddHostHidden(true); - setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" }); + setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth", rememberHost: false, storePassword: true }); } const handleAuthSubmit = (form) => { @@ -217,6 +296,7 @@ function App() { const handleSaveHost = () => { let hostConfig = { name: addHostForm.name || addHostForm.ip, + folder: addHostForm.folder, ip: addHostForm.ip, user: addHostForm.user, password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined, @@ -235,15 +315,32 @@ function App() { if (sessionToken) { userRef.current.loginUser({ sessionToken, - onSuccess, - onFailure, + onSuccess: () => { + setIsLoginUserHidden(true); + setIsLoggingIn(false); + if (onSuccess) onSuccess(); + }, + onFailure: (error) => { + localStorage.removeItem('sessionToken'); + setIsLoginUserHidden(false); + setIsLoggingIn(false); + if (onFailure) onFailure(error); + }, }); } else { userRef.current.loginUser({ username, password, - onSuccess, - onFailure, + onSuccess: () => { + setIsLoginUserHidden(true); + setIsLoggingIn(false); + if (onSuccess) onSuccess(); + }, + onFailure: (error) => { + setIsLoginUserHidden(false); + setIsLoggingIn(false); + if (onFailure) onFailure(error); + }, }); } } @@ -264,7 +361,7 @@ function App() { onFailure, }); } - } + }; const handleDeleteUser = ({ onSuccess, onFailure }) => { if (userRef.current) { @@ -311,31 +408,21 @@ function App() { } }; - const handleEditHost = async () => { + const handleEditHost = async (oldConfig, newConfig = null) => { try { - // Only clear the password if switching to RSA or storePassword is false - if (editHostForm.authMethod === 'rsaKey') { - editHostForm.password = ''; - } else if (!editHostForm.storePassword) { - editHostForm.password = ''; + if (newConfig) { + await userRef.current.editHost({ + oldHostConfig: oldConfig, + newHostConfig: newConfig, + }); + return; } - await userRef.current.editHost({ - oldHostConfig: currentHostConfig, - newHostConfig: editHostForm, - }); - - // Refresh the updated config - const refreshedHosts = await userRef.current.getAllHosts(); - const updated = refreshedHosts.find( - (h) => h.config.ip === editHostForm.ip && h.config.user === editHostForm.user - ); - if (updated) { - setCurrentHostConfig(updated.config); - } - setIsEditHostHidden(true); + updateEditHostForm(oldConfig); } catch (error) { - alert('Edit failed: ' + error); + console.error('Edit failed:', error); + setErrorMessage(`Edit failed: ${error}`); + setIsErrorHidden(false); } }; @@ -398,88 +485,124 @@ function App() { - {/* Launchpad Button */} - + {/* Action Buttons */} +
+ {/* Launchpad Button */} + - {/* Add Host Button */} - + {/* Add Host Button */} + - {/* Profile Button */} - + {/* Profile Button */} + +
{/* Terminal Views */}
- {terminals.map((terminal) => ( -
- ( +
{ - terminal.terminalRef = ref; + className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${ + splitTabIds.includes(terminal.id) || activeTab === terminal.id ? "block" : "hidden" + } flex-1`} + style={{ + order: splitTabIds.includes(terminal.id) + ? splitTabIds.indexOf(terminal.id) + : 0, }} - /> + > + { + terminal.terminalRef = ref; + }} + /> +
+ )) + ) : ( +
+
+

Welcome to Termix

+

{isLoggingIn ? "Checking login status..." : "Please login to start managing your SSH connections"}

+
- ))} + )}
-
- {/* Modals */} - - - - - - {isLaunchpadOpen && ( - setIsLaunchpadOpen(false)} - getHosts={getHosts} - connectToHost={connectToHostWithConfig} - isAddHostHidden={isAddHostHidden} - setIsAddHostHidden={setIsAddHostHidden} - isEditHostHidden={isEditHostHidden} - isErrorHidden={isErrorHidden} - deleteHost={deleteHost} - editHost={updateEditHostForm} + {/* Modals */} + {userRef.current?.getUser() && ( + <> + + + + {isLaunchpadOpen && ( + setIsLaunchpadOpen(false)} + getHosts={getHosts} + connectToHost={connectToHostWithConfig} + isAddHostHidden={isAddHostHidden} + setIsAddHostHidden={setIsAddHostHidden} + isEditHostHidden={isEditHostHidden} + isErrorHidden={isErrorHidden} + deleteHost={deleteHost} + editHost={handleEditHost} + /> + )} + + )} + + - )} - + - {/* User component */} - setIsLoginUserHidden(true)} - onCreateSuccess={() => { - setIsCreateUserHidden(true); - handleLoginUser({ username: createUserForm.username, password: createUserForm.password })} - } - onDeleteSuccess={() => { - setIsProfileHidden(true); - window.location.reload(); - }} - onFailure={(error) => { - setErrorMessage(`Action failed: ${error}`); - setIsErrorHidden(false); - }} - /> + + + {/* User component */} + { + setIsLoginUserHidden(true); + setIsLoggingIn(false); + setIsErrorHidden(true); + }} + onCreateSuccess={() => { + setIsCreateUserHidden(true); + handleLoginUser({ + username: createUserForm.username, + password: createUserForm.password, + onSuccess: () => { + setIsLoginUserHidden(true); + setIsLoggingIn(false); + setIsErrorHidden(true); + }, + onFailure: (error) => { + setErrorMessage(`Login failed: ${error}`); + setIsErrorHidden(false); + } + }); + }} + onDeleteSuccess={() => { + setIsProfileHidden(true); + window.location.reload(); + }} + onFailure={(error) => { + setErrorMessage(`Action failed: ${error}`); + setIsErrorHidden(false); + setIsLoggingIn(false); + }} + /> + ); diff --git a/src/apps/Launchpad.jsx b/src/apps/Launchpad.jsx index 00413bbf..03b2774f 100644 --- a/src/apps/Launchpad.jsx +++ b/src/apps/Launchpad.jsx @@ -4,20 +4,19 @@ import { CssVarsProvider } from '@mui/joy/styles'; import { Button } from '@mui/joy'; import HostViewerIcon from '../images/host_viewer_icon.png'; import theme from '../theme.js'; - -// Apps import HostViewer from './ssh/HostViewer.jsx'; -function Launchpad({onClose, - getHosts, - connectToHost, - isAddHostHidden, - setIsAddHostHidden, - isEditHostHidden, - isErrorHidden, - deleteHost, - editHost, - }) { +function Launchpad({ + onClose, + getHosts, + connectToHost, + isAddHostHidden, + setIsAddHostHidden, + isEditHostHidden, + isErrorHidden, + deleteHost, + editHost, +}) { const launchpadRef = useRef(null); const [sidebarOpen, setSidebarOpen] = useState(false); const [activeApp, setActiveApp] = useState('hostViewer'); @@ -42,11 +41,6 @@ function Launchpad({onClose, }; }, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]); - const handleEditHostClick = () => { - setIsAddHostHidden(false); - setActiveApp('hostViewer'); - }; - return (
{/* Main Content */} -
+
{activeApp === 'hostViewer' && ( )}
diff --git a/src/apps/ssh/HostViewer.jsx b/src/apps/ssh/HostViewer.jsx index 6a2bf683..7efb1ec5 100644 --- a/src/apps/ssh/HostViewer.jsx +++ b/src/apps/ssh/HostViewer.jsx @@ -2,11 +2,14 @@ import PropTypes from "prop-types"; import { useState, useEffect, useRef } from "react"; import { Button, Input } from "@mui/joy"; -function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost }) { +function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost, openEditPanel }) { const [hosts, setHosts] = useState([]); const [filteredHosts, setFilteredHosts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); + const [collapsedFolders, setCollapsedFolders] = useState(new Set()); + const [draggedHost, setDraggedHost] = useState(null); + const [isDraggingOver, setIsDraggingOver] = useState(null); const isMounted = useRef(true); const fetchHosts = async () => { @@ -44,11 +47,181 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e useEffect(() => { const filtered = hosts.filter((hostWrapper) => { const hostConfig = hostWrapper.config || {}; - return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) || hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()); + return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) || + hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()) || + hostConfig.folder?.toLowerCase().includes(searchTerm.toLowerCase()); }); setFilteredHosts(filtered); }, [searchTerm, hosts]); + const toggleFolder = (folderName) => { + setCollapsedFolders(prev => { + const newSet = new Set(prev); + if (newSet.has(folderName)) { + newSet.delete(folderName); + } else { + newSet.add(folderName); + } + return newSet; + }); + }; + + const groupHostsByFolder = (hosts) => { + const grouped = {}; + const noFolder = []; + + const sortedHosts = [...hosts].sort((a, b) => { + const nameA = (a.config?.name || a.config?.ip || '').toLowerCase(); + const nameB = (b.config?.name || b.config?.ip || '').toLowerCase(); + return nameA.localeCompare(nameB); + }); + + sortedHosts.forEach(host => { + const folder = host.config?.folder; + if (folder) { + if (!grouped[folder]) { + grouped[folder] = []; + } + grouped[folder].push(host); + } else { + noFolder.push(host); + } + }); + + const sortedFolders = Object.keys(grouped).sort((a, b) => a.localeCompare(b)); + + return { grouped, sortedFolders, noFolder }; + }; + + const handleDragStart = (e, host) => { + setDraggedHost(host); + e.dataTransfer.setData('text/plain', ''); + }; + + const handleDragOver = (e, folderName) => { + e.preventDefault(); + setIsDraggingOver(folderName); + }; + + const handleDragLeave = () => { + setIsDraggingOver(null); + }; + + const handleDrop = async (e, targetFolder) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingOver(null); + + if (!draggedHost) return; + + if (draggedHost.config.folder === targetFolder) return; + + const newConfig = { + ...draggedHost.config, + folder: targetFolder + }; + + try { + await editHost(draggedHost.config, newConfig); + await fetchHosts(); + } catch (error) { + console.error('Failed to update folder:', error); + } + + setDraggedHost(null); + }; + + const handleDropOnNoFolder = async (e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingOver(null); + + if (!draggedHost || !draggedHost.config.folder) return; + + const newConfig = { + ...draggedHost.config, + folder: null + }; + + try { + await editHost(draggedHost.config, newConfig); + await fetchHosts(); + } catch (error) { + console.error('Failed to remove from folder:', error); + } + + setDraggedHost(null); + }; + + const renderHostItem = (hostWrapper) => { + const hostConfig = hostWrapper.config || {}; + + if (!hostConfig) { + return null; + } + + return ( +
handleDragStart(e, hostWrapper)} + onDragEnd={() => setDraggedHost(null)} + > +
+
⋮⋮
+
+

{hostConfig.name || hostConfig.ip}

+

+ {hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`} +

+
+
+
+ + + +
+
+ ); + }; + return (
@@ -79,60 +252,51 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e

Loading hosts...

) : filteredHosts.length > 0 ? (
- {filteredHosts.map((hostWrapper, index) => { - const hostConfig = hostWrapper.config || {}; - - if (!hostConfig) { - return null; - } + {(() => { + const { grouped, sortedFolders, noFolder } = groupHostsByFolder(filteredHosts); return ( -
-
-

{hostConfig.name || hostConfig.ip}

-

- {hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`} -

+ <> + {/* Render hosts without folders first */} +
handleDragOver(e, 'no-folder')} + onDragLeave={handleDragLeave} + onDrop={handleDropOnNoFolder} + > + {noFolder.map((host) => renderHostItem(host))}
-
- - - -
-
+ + {/* Render folders and their hosts */} + {sortedFolders.map((folderName) => ( +
+
toggleFolder(folderName)} + onDragOver={(e) => handleDragOver(e, folderName)} + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, folderName)} + > + + ▼ + + {folderName} + + ({grouped[folderName].length}) + +
+ {!collapsedFolders.has(folderName) && ( +
+ {grouped[folderName].map((host) => renderHostItem(host))} +
+ )} +
+ ))} + ); - })} + })()}
) : (

No hosts available...

@@ -148,6 +312,7 @@ HostViewer.propTypes = { setIsAddHostHidden: PropTypes.func.isRequired, deleteHost: PropTypes.func.isRequired, editHost: PropTypes.func.isRequired, + openEditPanel: PropTypes.func.isRequired, }; export default HostViewer; \ No newline at end of file diff --git a/src/backend/database.cjs b/src/backend/database.cjs index f5bc9c87..8d2f7167 100644 --- a/src/backend/database.cjs +++ b/src/backend/database.cjs @@ -28,7 +28,8 @@ const hostSchema = new mongoose.Schema({ name: { type: String, required: true }, config: { type: String, required: true }, users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], - createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } + createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + folder: { type: String, default: null } }); const User = mongoose.model('User', userSchema); @@ -178,7 +179,8 @@ io.of('/database.io').on('connection', (socket) => { } const cleanConfig = { - name: hostConfig.name.trim(), + name: hostConfig.name?.trim(), + folder: hostConfig.folder?.trim() || null, ip: hostConfig.ip.trim(), user: hostConfig.user.trim(), port: hostConfig.port || 22, @@ -208,7 +210,8 @@ io.of('/database.io').on('connection', (socket) => { name: finalName, config: encryptedConfig, users: [userId], - createdBy: userId + createdBy: userId, + folder: cleanConfig.folder }); logger.info(`Host created successfully: ${finalName}`); @@ -360,10 +363,11 @@ io.of('/database.io').on('connection', (socket) => { } const cleanConfig = { + name: newHostConfig.name?.trim(), + folder: newHostConfig.folder?.trim() || null, ip: newHostConfig.ip.trim(), user: newHostConfig.user.trim(), port: newHostConfig.port || 22, - name: newHostConfig.name.trim(), password: newHostConfig.password?.trim() || undefined, rsaKey: newHostConfig.rsaKey?.trim() || undefined }; @@ -375,6 +379,7 @@ io.of('/database.io').on('connection', (socket) => { } host.config = encryptedConfig; + host.folder = cleanConfig.folder; await host.save(); logger.info(`Host edited successfully`); diff --git a/src/modals/AddHostModal.jsx b/src/modals/AddHostModal.jsx index 046c8b1d..71955c51 100644 --- a/src/modals/AddHostModal.jsx +++ b/src/modals/AddHostModal.jsx @@ -13,7 +13,11 @@ import { Select, Option, Checkbox, - IconButton + IconButton, + Tabs, + TabList, + Tab, + TabPanel } from '@mui/joy'; import theme from '/src/theme'; import { useState } from 'react'; @@ -22,6 +26,7 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => { const [showPassword, setShowPassword] = useState(false); + const [activeTab, setActiveTab] = useState(0); const handleFileChange = (e) => { const file = e.target.files[0]; @@ -68,7 +73,6 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd setIsAddHostHidden(true)} sx={{ - overflow: 'hidden', display: 'flex', justifyContent: 'center', alignItems: 'center', @@ -82,172 +86,258 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd color: theme.palette.text.primary, padding: 3, borderRadius: 10, - maxWidth: '400px', + maxWidth: '500px', width: '100%', - overflow: 'hidden', + maxHeight: '80vh', + overflow: 'auto', boxSizing: 'border-box', mx: 2, }} > - Add Host + Add Host
- - - Host Name - setForm({ ...form, name: e.target.value })} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - }} - /> - - - Host IP - setForm({ ...form, ip: e.target.value })} - required - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - }} - /> - - - Host User - setForm({ ...form, user: e.target.value })} - required - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - }} - /> - - - Authentication Method - - - {form.authMethod === 'password' && ( - - Host Password -
+ '&.Mui-selected': { + bgcolor: theme.palette.general.primary, + color: theme.palette.text.primary, + '&:hover': { + bgcolor: theme.palette.general.primary, + }, + }, + }, + }} + > + Basic Info + Connection + Authentication + + + + + + Host Name setForm({ ...form, password: e.target.value })} + value={form.name} + onChange={(e) => setForm({ ...form, name: e.target.value })} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + Folder + setForm({ ...form, folder: e.target.value })} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + + + + + + Host IP + setForm({ ...form, ip: e.target.value })} required sx={{ backgroundColor: theme.palette.general.primary, color: theme.palette.text.primary, - flex: 1, }} /> - setShowPassword(!showPassword)} + + + Host User + setForm({ ...form, user: e.target.value })} + required + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + 65535}> + Host Port + setForm({ ...form, port: e.target.value })} + min={1} + max={65535} + required + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + }} + /> + + + + + + + + Remember Host + setForm({ ...form, rememberHost: e.target.checked })} sx={{ color: theme.palette.text.primary, - marginLeft: 1, + '&.Mui-checked': { + color: theme.palette.text.primary, + }, }} - > - {showPassword ? : } - -
-
- )} - {form.authMethod === 'rsaKey' && ( - - RSA Key - - - )} - 65535}> - Host Port - setForm({ ...form, port: e.target.value })} - min={1} - max={65535} - required - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - }} - /> - - - Remember Host - setForm({ ...form, rememberHost: e.target.checked })} - sx={{ - color: theme.palette.text.primary, - '&.Mui-checked': { - color: theme.palette.text.primary, - }, - }} - /> - - {form.rememberHost && ( - - Store Password - setForm({ ...form, storePassword: e.target.checked })} - sx={{ - color: theme.palette.text.primary, - '&.Mui-checked': { - color: theme.palette.text.primary, - }, - }} - /> - - )} - -
+ /> + + {form.rememberHost && ( + <> + + Store Password + setForm({ ...form, storePassword: e.target.checked })} + sx={{ + color: theme.palette.text.primary, + '&.Mui-checked': { + color: theme.palette.text.primary, + }, + }} + /> + + + Authentication Method + + + {form.authMethod === 'password' && ( + + Host Password +
+ setForm({ ...form, password: e.target.value })} + required + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + flex: 1, + }} + /> + setShowPassword(!showPassword)} + sx={{ + color: theme.palette.text.primary, + marginLeft: 1, + }} + > + {showPassword ? : } + +
+
+ )} + {form.authMethod === 'rsaKey' && ( + + RSA Key + + + )} + + )} + + + + +
@@ -260,6 +350,7 @@ AddHostModal.propTypes = { isHidden: PropTypes.bool.isRequired, form: PropTypes.shape({ name: PropTypes.string, + folder: PropTypes.string, ip: PropTypes.string.isRequired, user: PropTypes.string.isRequired, password: PropTypes.string, diff --git a/src/modals/EditHostModal.jsx b/src/modals/EditHostModal.jsx index ab0096ff..411dbe98 100644 --- a/src/modals/EditHostModal.jsx +++ b/src/modals/EditHostModal.jsx @@ -14,7 +14,11 @@ import { Select, Option, IconButton, - Checkbox + Checkbox, + Tabs, + TabList, + Tab, + TabPanel } from '@mui/joy'; import theme from '/src/theme'; import Visibility from '@mui/icons-material/Visibility'; @@ -22,25 +26,23 @@ 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); useEffect(() => { - if (hostConfig) { - const storePassword = hostConfig.password || hostConfig.rsaKey; - + if (hostConfig && !isHidden) { setForm({ - ...form, - name: hostConfig.name || '', - ip: hostConfig.ip || '', - user: hostConfig.user || '', - password: storePassword && hostConfig.password ? hostConfig.password : '', - rsaKey: '', - port: Number(hostConfig.port) || 22, - authMethod: hostConfig.rsaKey ? 'rsaKey' : (storePassword ? 'password' : 'Select Auth'), - rememberHost: hostConfig.rememberHost || true, - storePassword: storePassword ?? false + name: hostConfig.name || "", + folder: hostConfig.folder || "", + ip: hostConfig.ip || "", + user: hostConfig.user || "", + password: hostConfig.password || "", + port: hostConfig.port || 22, + authMethod: hostConfig.password ? "password" : hostConfig.rsaKey ? "rsaKey" : "Select Auth", + rememberHost: true, + storePassword: true, }); } - }, [hostConfig, setForm]); + }, [hostConfig, isHidden]); const handleFileChange = (e) => { const file = e.target.files[0]; @@ -65,7 +67,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH const handleStorePasswordChange = (checked) => { setForm((prev) => ({ ...prev, - storePassword: checked, + storePassword: Boolean(checked), authMethod: checked ? 'password' : 'Select Auth' })); }; @@ -76,35 +78,42 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH const portNum = Number(port); if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false; - if (storePassword && authMethod === 'password' && !password.trim()) return false; - if (storePassword && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false; - if (storePassword && authMethod === 'Select Auth') return false; + if (Boolean(storePassword) && authMethod === 'password' && !password?.trim()) return false; + if (Boolean(storePassword) && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false; + if (Boolean(storePassword) && authMethod === 'Select Auth') return false; return true; }; - const handleSubmit = (e) => { + const handleSave = async (e) => { e.preventDefault(); - if (isFormValid()) { - const { authMethod, password, rsaKey, storePassword, ...rest } = form; - handleEditHost({ - ...rest, - authMethod, - password: authMethod === 'password' && storePassword ? password : '', - rsaKey: authMethod === 'rsaKey' ? rsaKey : '' - }); + try { + const newConfig = { + ...form, + 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); } }; return ( - setIsEditHostHidden(true)} - sx={{ - overflowX: 'hidden', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - }} + setIsEditHostHidden(true)} + sx={{ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }} > - Edit Host + Edit Host -
- - - Host Name - setForm((prev) => ({ ...prev, name: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - - - - Host IP - setForm((prev) => ({ ...prev, ip: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - - - - Host User - setForm((prev) => ({ ...prev, user: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - - - {form.storePassword && form.authMethod !== 'Select Auth' && ( - - Authentication Method - - - )} - - {form.authMethod === 'password' && form.storePassword && ( - - Password -
- - setForm((prev) => ({ ...prev, password: e.target.value })) - } - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - flex: 1 - }} - /> - setShowPassword(!showPassword)} - sx={{ - color: theme.palette.text.primary, - marginLeft: 1 - }} - > - {showPassword ? : } - -
-
- )} - - {form.authMethod === 'rsaKey' && form.storePassword && ( - - RSA Key - - {hostConfig?.rsaKey && !form.rsaKey && ( - - Existing key detected. Upload to replace. - - )} - - )} - - 65535}> - Host Port - setForm((prev) => ({ ...prev, port: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary - }} - /> - - - - Store Password - handleStorePasswordChange(e.target.checked)} - sx={{ - color: theme.palette.text.primary, - '&.Mui-checked': { - color: theme.palette.text.primary - } - }} - /> - - - -
+ Basic Info + Connection + Authentication + + + + + + Host Name + setForm((prev) => ({ ...prev, name: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary + }} + /> + + + + Folder + setForm((prev) => ({ ...prev, folder: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary + }} + /> + + + + + + + + Host IP + setForm((prev) => ({ ...prev, ip: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary + }} + /> + + + 65535}> + Host Port + setForm((prev) => ({ ...prev, port: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary + }} + /> + + + + Host User + setForm((prev) => ({ ...prev, user: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary + }} + /> + + + + + + + + Store Password + handleStorePasswordChange(e.target.checked)} + sx={{ + color: theme.palette.text.primary, + '&.Mui-checked': { + color: theme.palette.text.primary + } + }} + /> + + + {form.storePassword && ( + + Authentication Method + + + )} + + {form.authMethod === 'password' && form.storePassword && ( + + Password +
+ setForm((prev) => ({ ...prev, password: e.target.value }))} + sx={{ + backgroundColor: theme.palette.general.primary, + color: theme.palette.text.primary, + flex: 1 + }} + /> + setShowPassword(!showPassword)} + sx={{ + color: theme.palette.text.primary, + marginLeft: 1 + }} + > + {showPassword ? : } + +
+
+ )} + + {form.authMethod === 'rsaKey' && form.storePassword && ( + + RSA Key + + {hostConfig?.rsaKey && !form.rsaKey && ( + + Existing key detected. Upload to replace. + + )} + + )} +
+
+ + +
diff --git a/src/modals/ProfileModal.jsx b/src/modals/ProfileModal.jsx index 38e59239..d572fb26 100644 --- a/src/modals/ProfileModal.jsx +++ b/src/modals/ProfileModal.jsx @@ -1,101 +1,83 @@ import PropTypes from 'prop-types'; -import { CssVarsProvider } from '@mui/joy/styles'; -import { Modal, Button, DialogTitle, DialogContent, ModalDialog, Stack } from '@mui/joy'; -import theme from '/src/theme'; - -const ProfileModal = ({ isHidden, getUser, handleDeleteUser, handleLogoutUser, setIsProfileHidden }) => { - const handleDelete = () => { - handleDeleteUser({ - onSuccess: () => { - window.location.reload(); - } - }); - }; - - const handleLogout = () => { - handleLogoutUser({ - onSuccess: () => { - window.location.reload(); - } - }); - } - - const getUserName = () => { - const user = getUser(); - return user ? user.username : ''; - } +import { Modal, Typography, Button } from "@mui/joy"; +import LogoutIcon from "@mui/icons-material/Logout"; +import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; +import AccountCircleIcon from "@mui/icons-material/AccountCircle"; +import theme from "../theme"; +export default function ProfileModal({ + isHidden, + getUser, + handleDeleteUser, + handleLogoutUser, + setIsProfileHidden, +}) { return ( - - setIsProfileHidden(true)}> - - setIsProfileHidden(true)} + sx={{ + display: "flex", + justifyContent: "center", + alignItems: "center", + }} + > +
+
+ - - - - - - + Logout + + + +
+
+
); -}; +} ProfileModal.propTypes = { isHidden: PropTypes.bool.isRequired, @@ -103,6 +85,4 @@ ProfileModal.propTypes = { handleDeleteUser: PropTypes.func.isRequired, handleLogoutUser: PropTypes.func.isRequired, setIsProfileHidden: PropTypes.func.isRequired, -}; - -export default ProfileModal; \ No newline at end of file +}; \ No newline at end of file