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.)

This commit is contained in:
Karmaa
2025-03-16 01:51:14 -05:00
parent fd966b9954
commit fda8c7ce4b
7 changed files with 1162 additions and 692 deletions

View File

@@ -31,6 +31,7 @@ function App() {
const [nextId, setNextId] = useState(1); const [nextId, setNextId] = useState(1);
const [addHostForm, setAddHostForm] = useState({ const [addHostForm, setAddHostForm] = useState({
name: "", name: "",
folder: "",
ip: "", ip: "",
user: "", user: "",
password: "", password: "",
@@ -41,6 +42,7 @@ function App() {
}); });
const [editHostForm, setEditHostForm] = useState({ const [editHostForm, setEditHostForm] = useState({
name: "", name: "",
folder: "",
ip: "", ip: "",
user: "", user: "",
password: "", password: "",
@@ -66,6 +68,7 @@ function App() {
const [splitTabIds, setSplitTabIds] = useState([]); const [splitTabIds, setSplitTabIds] = useState([]);
const [isEditHostHidden, setIsEditHostHidden] = useState(true); const [isEditHostHidden, setIsEditHostHidden] = useState(true);
const [currentHostConfig, setCurrentHostConfig] = useState(null); const [currentHostConfig, setCurrentHostConfig] = useState(null);
const [isLoggingIn, setIsLoggingIn] = useState(true);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e) => { const handleKeyDown = (e) => {
@@ -124,23 +127,97 @@ function App() {
useEffect(() => { useEffect(() => {
const sessionToken = localStorage.getItem('sessionToken'); const sessionToken = localStorage.getItem('sessionToken');
if (sessionToken) { let isComponentMounted = true;
setTimeout(() => { let isLoginInProgress = false;
handleLoginUser({
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, sessionToken,
onSuccess: () => { onSuccess: () => {
if (isComponentMounted) {
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
setIsLoginUserHidden(true); setIsLoginUserHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
}
isLoginInProgress = false;
}, },
onFailure: (error) => { onFailure: (error) => {
if (isComponentMounted) {
if (!userRef.current?.getUser()) {
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
localStorage.removeItem('sessionToken');
setErrorMessage(`Auto-login failed: ${error}`); setErrorMessage(`Auto-login failed: ${error}`);
setIsErrorHidden(false); setIsErrorHidden(false);
setIsLoginUserHidden(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 = () => { const handleAddHost = () => {
@@ -168,6 +245,8 @@ function App() {
id: nextId, id: nextId,
title: addHostForm.name || addHostForm.ip, title: addHostForm.name || addHostForm.ip,
hostConfig: { hostConfig: {
name: addHostForm.name,
folder: addHostForm.folder,
ip: addHostForm.ip, ip: addHostForm.ip,
user: addHostForm.user, user: addHostForm.user,
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined, password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
@@ -180,7 +259,7 @@ function App() {
setActiveTab(nextId); setActiveTab(nextId);
setNextId(nextId + 1); setNextId(nextId + 1);
setIsAddHostHidden(true); 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) => { const handleAuthSubmit = (form) => {
@@ -217,6 +296,7 @@ function App() {
const handleSaveHost = () => { const handleSaveHost = () => {
let hostConfig = { let hostConfig = {
name: addHostForm.name || addHostForm.ip, name: addHostForm.name || addHostForm.ip,
folder: addHostForm.folder,
ip: addHostForm.ip, ip: addHostForm.ip,
user: addHostForm.user, user: addHostForm.user,
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined, password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
@@ -235,15 +315,32 @@ function App() {
if (sessionToken) { if (sessionToken) {
userRef.current.loginUser({ userRef.current.loginUser({
sessionToken, sessionToken,
onSuccess, onSuccess: () => {
onFailure, setIsLoginUserHidden(true);
setIsLoggingIn(false);
if (onSuccess) onSuccess();
},
onFailure: (error) => {
localStorage.removeItem('sessionToken');
setIsLoginUserHidden(false);
setIsLoggingIn(false);
if (onFailure) onFailure(error);
},
}); });
} else { } else {
userRef.current.loginUser({ userRef.current.loginUser({
username, username,
password, password,
onSuccess, onSuccess: () => {
onFailure, setIsLoginUserHidden(true);
setIsLoggingIn(false);
if (onSuccess) onSuccess();
},
onFailure: (error) => {
setIsLoginUserHidden(false);
setIsLoggingIn(false);
if (onFailure) onFailure(error);
},
}); });
} }
} }
@@ -264,7 +361,7 @@ function App() {
onFailure, onFailure,
}); });
} }
} };
const handleDeleteUser = ({ onSuccess, onFailure }) => { const handleDeleteUser = ({ onSuccess, onFailure }) => {
if (userRef.current) { if (userRef.current) {
@@ -311,31 +408,21 @@ function App() {
} }
}; };
const handleEditHost = async () => { const handleEditHost = async (oldConfig, newConfig = null) => {
try { try {
// Only clear the password if switching to RSA or storePassword is false if (newConfig) {
if (editHostForm.authMethod === 'rsaKey') {
editHostForm.password = '';
} else if (!editHostForm.storePassword) {
editHostForm.password = '';
}
await userRef.current.editHost({ await userRef.current.editHost({
oldHostConfig: currentHostConfig, oldHostConfig: oldConfig,
newHostConfig: editHostForm, newHostConfig: newConfig,
}); });
return;
// 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) { } catch (error) {
alert('Edit failed: ' + error); console.error('Edit failed:', error);
setErrorMessage(`Edit failed: ${error}`);
setIsErrorHidden(false);
} }
}; };
@@ -398,8 +485,11 @@ function App() {
</div> </div>
</div> </div>
{/* Action Buttons */}
<div className="flex gap-4">
{/* Launchpad Button */} {/* Launchpad Button */}
<Button <Button
disabled={isLoggingIn || !userRef.current?.getUser()}
onClick={() => setIsLaunchpadOpen(true)} onClick={() => setIsLaunchpadOpen(true)}
sx={{ sx={{
backgroundColor: theme.palette.general.tertiary, backgroundColor: theme.palette.general.tertiary,
@@ -408,33 +498,21 @@ function App() {
height: "52px", height: "52px",
width: "52px", width: "52px",
padding: 0, padding: 0,
opacity: (!userRef.current?.getUser() || isLoggingIn) ? 0.3 : 1,
cursor: (!userRef.current?.getUser() || isLoggingIn) ? 'not-allowed' : 'pointer',
"&:disabled": {
opacity: 0.3,
backgroundColor: theme.palette.general.tertiary,
}
}} }}
> >
<img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70", objectFit: "contain" }} /> <img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70%", objectFit: "contain" }} />
</Button> </Button>
{/* Add Host Button */} {/* Add Host Button */}
<Button <Button
disabled={isLoggingIn || !userRef.current?.getUser()}
onClick={() => setIsAddHostHidden(false)} onClick={() => setIsAddHostHidden(false)}
sx={{
backgroundColor: theme.palette.general.tertiary,
"&:hover": { backgroundColor: theme.palette.general.secondary },
flexShrink: 0,
height: "52px",
width: "52px",
fontSize: "3.5rem",
display: "flex",
justifyContent: "center",
alignItems: "center",
paddingTop: "2px",
}}
>
+
</Button>
{/* Profile Button */}
<Button
onClick={() => setIsProfileHidden(false)}
sx={{ sx={{
backgroundColor: theme.palette.general.tertiary, backgroundColor: theme.palette.general.tertiary,
"&:hover": { backgroundColor: theme.palette.general.secondary }, "&:hover": { backgroundColor: theme.palette.general.secondary },
@@ -445,6 +523,41 @@ function App() {
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
padding: 0, padding: 0,
opacity: (!userRef.current?.getUser() || isLoggingIn) ? 0.3 : 1,
cursor: (!userRef.current?.getUser() || isLoggingIn) ? 'not-allowed' : 'pointer',
"&:disabled": {
opacity: 0.3,
backgroundColor: theme.palette.general.tertiary,
},
fontSize: "4rem",
fontWeight: "600",
lineHeight: "0",
paddingBottom: "8px",
}}
>
+
</Button>
{/* Profile Button */}
<Button
disabled={isLoggingIn}
onClick={() => userRef.current?.getUser() ? setIsProfileHidden(false) : setIsLoginUserHidden(false)}
sx={{
backgroundColor: theme.palette.general.tertiary,
"&:hover": { backgroundColor: theme.palette.general.secondary },
flexShrink: 0,
height: "52px",
width: "52px",
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: 0,
opacity: isLoggingIn ? 0.3 : 1,
cursor: isLoggingIn ? 'not-allowed' : 'pointer',
"&:disabled": {
opacity: 0.3,
backgroundColor: theme.palette.general.tertiary,
}
}} }}
> >
<img <img
@@ -454,10 +567,12 @@ function App() {
/> />
</Button> </Button>
</div> </div>
</div>
{/* Terminal Views */} {/* Terminal Views */}
<div className={`relative p-4 terminal-container ${getLayoutStyle()}`}> <div className={`relative p-4 terminal-container ${getLayoutStyle()}`}>
{terminals.map((terminal) => ( {userRef.current?.getUser() ? (
terminals.map((terminal) => (
<div <div
key={terminal.id} key={terminal.id}
className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${ className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${
@@ -479,7 +594,15 @@ function App() {
}} }}
/> />
</div> </div>
))} ))
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center text-neutral-400">
<h2 className="text-2xl font-bold mb-4">Welcome to Termix</h2>
<p>{isLoggingIn ? "Checking login status..." : "Please login to start managing your SSH connections"}</p>
</div>
</div>
)}
<NoAuthenticationModal <NoAuthenticationModal
isHidden={isNoAuthHidden} isHidden={isNoAuthHidden}
form={authForm} form={authForm}
@@ -488,9 +611,10 @@ function App() {
handleAuthSubmit={handleAuthSubmit} handleAuthSubmit={handleAuthSubmit}
/> />
</div> </div>
</div>
{/* Modals */} {/* Modals */}
{userRef.current?.getUser() && (
<>
<AddHostModal <AddHostModal
isHidden={isAddHostHidden} isHidden={isAddHostHidden}
form={addHostForm} form={addHostForm}
@@ -506,14 +630,6 @@ function App() {
setIsEditHostHidden={setIsEditHostHidden} setIsEditHostHidden={setIsEditHostHidden}
hostConfig={currentHostConfig} hostConfig={currentHostConfig}
/> />
<CreateUserModal
isHidden={isCreateUserHidden}
form={createUserForm}
setForm={setCreateUserForm}
handleCreateUser={handleCreateUser}
setIsCreateUserHidden={setIsCreateUserHidden}
setIsLoginUserHidden={setIsLoginUserHidden}
/>
<ProfileModal <ProfileModal
isHidden={isProfileHidden} isHidden={isProfileHidden}
getUser={getUser} getUser={getUser}
@@ -521,11 +637,6 @@ function App() {
handleLogoutUser={handleLogoutUser} handleLogoutUser={handleLogoutUser}
setIsProfileHidden={setIsProfileHidden} setIsProfileHidden={setIsProfileHidden}
/> />
<ErrorModal
isHidden={isErrorHidden}
errorMessage={errorMessage}
setIsErrorHidden={setIsErrorHidden}
/>
{isLaunchpadOpen && ( {isLaunchpadOpen && (
<Launchpad <Launchpad
onClose={() => setIsLaunchpadOpen(false)} onClose={() => setIsLaunchpadOpen(false)}
@@ -536,9 +647,17 @@ function App() {
isEditHostHidden={isEditHostHidden} isEditHostHidden={isEditHostHidden}
isErrorHidden={isErrorHidden} isErrorHidden={isErrorHidden}
deleteHost={deleteHost} deleteHost={deleteHost}
editHost={updateEditHostForm} editHost={handleEditHost}
/> />
)} )}
</>
)}
<ErrorModal
isHidden={isErrorHidden}
errorMessage={errorMessage}
setIsErrorHidden={setIsErrorHidden}
/>
<LoginUserModal <LoginUserModal
isHidden={isLoginUserHidden} isHidden={isLoginUserHidden}
@@ -550,14 +669,39 @@ function App() {
setIsCreateUserHidden={setIsCreateUserHidden} setIsCreateUserHidden={setIsCreateUserHidden}
/> />
<CreateUserModal
isHidden={isCreateUserHidden}
form={createUserForm}
setForm={setCreateUserForm}
handleCreateUser={handleCreateUser}
setIsCreateUserHidden={setIsCreateUserHidden}
setIsLoginUserHidden={setIsLoginUserHidden}
/>
{/* User component */} {/* User component */}
<User <User
ref={userRef} ref={userRef}
onLoginSuccess={() => setIsLoginUserHidden(true)} onLoginSuccess={() => {
setIsLoginUserHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
}}
onCreateSuccess={() => { onCreateSuccess={() => {
setIsCreateUserHidden(true); setIsCreateUserHidden(true);
handleLoginUser({ username: createUserForm.username, password: createUserForm.password })} handleLoginUser({
username: createUserForm.username,
password: createUserForm.password,
onSuccess: () => {
setIsLoginUserHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
},
onFailure: (error) => {
setErrorMessage(`Login failed: ${error}`);
setIsErrorHidden(false);
} }
});
}}
onDeleteSuccess={() => { onDeleteSuccess={() => {
setIsProfileHidden(true); setIsProfileHidden(true);
window.location.reload(); window.location.reload();
@@ -565,9 +709,11 @@ function App() {
onFailure={(error) => { onFailure={(error) => {
setErrorMessage(`Action failed: ${error}`); setErrorMessage(`Action failed: ${error}`);
setIsErrorHidden(false); setIsErrorHidden(false);
setIsLoggingIn(false);
}} }}
/> />
</div> </div>
</div>
</CssVarsProvider> </CssVarsProvider>
); );
} }

View File

@@ -4,11 +4,10 @@ import { CssVarsProvider } from '@mui/joy/styles';
import { Button } from '@mui/joy'; import { Button } from '@mui/joy';
import HostViewerIcon from '../images/host_viewer_icon.png'; import HostViewerIcon from '../images/host_viewer_icon.png';
import theme from '../theme.js'; import theme from '../theme.js';
// Apps
import HostViewer from './ssh/HostViewer.jsx'; import HostViewer from './ssh/HostViewer.jsx';
function Launchpad({onClose, function Launchpad({
onClose,
getHosts, getHosts,
connectToHost, connectToHost,
isAddHostHidden, isAddHostHidden,
@@ -17,7 +16,7 @@ function Launchpad({onClose,
isErrorHidden, isErrorHidden,
deleteHost, deleteHost,
editHost, editHost,
}) { }) {
const launchpadRef = useRef(null); const launchpadRef = useRef(null);
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const [activeApp, setActiveApp] = useState('hostViewer'); const [activeApp, setActiveApp] = useState('hostViewer');
@@ -42,11 +41,6 @@ function Launchpad({onClose,
}; };
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]); }, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]);
const handleEditHostClick = () => {
setIsAddHostHidden(false);
setActiveApp('hostViewer');
};
return ( return (
<CssVarsProvider theme={theme}> <CssVarsProvider theme={theme}>
<div <div
@@ -163,7 +157,7 @@ function Launchpad({onClose,
</div> </div>
{/* Main Content */} {/* Main Content */}
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}> <div style={{ flex: 1, overflow: 'hidden' }}>
{activeApp === 'hostViewer' && ( {activeApp === 'hostViewer' && (
<HostViewer <HostViewer
getHosts={getHosts} getHosts={getHosts}
@@ -171,7 +165,7 @@ function Launchpad({onClose,
setIsAddHostHidden={setIsAddHostHidden} setIsAddHostHidden={setIsAddHostHidden}
deleteHost={deleteHost} deleteHost={deleteHost}
editHost={editHost} editHost={editHost}
onEditHostClick={handleEditHostClick} openEditPanel={editHost}
/> />
)} )}
</div> </div>

View File

@@ -2,11 +2,14 @@ import PropTypes from "prop-types";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { Button, Input } from "@mui/joy"; 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 [hosts, setHosts] = useState([]);
const [filteredHosts, setFilteredHosts] = useState([]); const [filteredHosts, setFilteredHosts] = useState([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState(""); 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 isMounted = useRef(true);
const fetchHosts = async () => { const fetchHosts = async () => {
@@ -44,11 +47,181 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
useEffect(() => { useEffect(() => {
const filtered = hosts.filter((hostWrapper) => { const filtered = hosts.filter((hostWrapper) => {
const hostConfig = hostWrapper.config || {}; 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); setFilteredHosts(filtered);
}, [searchTerm, hosts]); }, [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 (
<div
key={hostWrapper._id}
className={`flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 w-full cursor-grab active:cursor-grabbing hover:border-neutral-500 transition-colors ${draggedHost === hostWrapper ? 'opacity-50' : ''}`}
draggable
onDragStart={(e) => handleDragStart(e, hostWrapper)}
onDragEnd={() => setDraggedHost(null)}
>
<div className="flex items-center gap-2 flex-1">
<div className="text-neutral-500 cursor-grab active:cursor-grabbing"></div>
<div>
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p>
<p className="text-sm text-gray-400">
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`}
</p>
</div>
</div>
<div className="flex gap-2">
<Button
className="text-black"
onClick={(e) => {
e.stopPropagation();
connectToHost(hostConfig);
}}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Connect
</Button>
<Button
className="text-black"
onClick={(e) => {
e.stopPropagation();
deleteHost({ ...hostConfig, _id: hostWrapper._id });
}}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Delete
</Button>
<Button
className="text-black"
onClick={(e) => {
e.stopPropagation();
openEditPanel(hostConfig);
}}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Edit
</Button>
</div>
</div>
);
};
return ( return (
<div className="h-full w-full p-4 text-white flex flex-col"> <div className="h-full w-full p-4 text-white flex flex-col">
<div className="flex items-center justify-between mb-2 w-full gap-2"> <div className="flex items-center justify-between mb-2 w-full gap-2">
@@ -79,60 +252,51 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
<p className="text-gray-300">Loading hosts...</p> <p className="text-gray-300">Loading hosts...</p>
) : filteredHosts.length > 0 ? ( ) : filteredHosts.length > 0 ? (
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
{filteredHosts.map((hostWrapper, index) => { {(() => {
const hostConfig = hostWrapper.config || {}; const { grouped, sortedFolders, noFolder } = groupHostsByFolder(filteredHosts);
if (!hostConfig) {
return null;
}
return ( return (
<div key={index} className="flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 w-full"> <>
<div> {/* Render hosts without folders first */}
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p> <div
<p className="text-sm text-gray-400"> className={`flex flex-col gap-2 p-2 rounded-lg transition-colors ${isDraggingOver === 'no-folder' ? 'bg-neutral-700' : ''}`}
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`} onDragOver={(e) => handleDragOver(e, 'no-folder')}
</p> onDragLeave={handleDragLeave}
</div> onDrop={handleDropOnNoFolder}
<div className="flex gap-2">
<Button
className="text-black"
onClick={() => connectToHost(hostConfig)}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
> >
Connect {noFolder.map((host) => renderHostItem(host))}
</Button>
<Button
className="text-black"
onClick={() => {
deleteHost({ ...hostConfig, _id: hostWrapper._id });
}}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Delete
</Button>
<Button
className="text-black"
onClick={() => {
editHost(hostConfig);
}}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Edit
</Button>
</div> </div>
{/* Render folders and their hosts */}
{sortedFolders.map((folderName) => (
<div key={folderName} className="mb-2">
<div
className={`flex items-center gap-2 p-2 bg-neutral-600 rounded-lg cursor-pointer hover:bg-neutral-500 transition-colors ${
isDraggingOver === folderName ? 'bg-neutral-500 border-2 border-dashed border-neutral-400' : ''
}`}
onClick={() => toggleFolder(folderName)}
onDragOver={(e) => handleDragOver(e, folderName)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, folderName)}
>
<span className={`font-bold w-4 text-center transition-transform ${collapsedFolders.has(folderName) ? 'rotate-[-90deg]' : ''}`}>
</span>
<span className="font-bold">{folderName}</span>
<span className="text-sm text-gray-300">
({grouped[folderName].length})
</span>
</div> </div>
{!collapsedFolders.has(folderName) && (
<div className="ml-6 mt-2 flex flex-col gap-2">
{grouped[folderName].map((host) => renderHostItem(host))}
</div>
)}
</div>
))}
</>
); );
})} })()}
</div> </div>
) : ( ) : (
<p className="text-gray-300">No hosts available...</p> <p className="text-gray-300">No hosts available...</p>
@@ -148,6 +312,7 @@ HostViewer.propTypes = {
setIsAddHostHidden: PropTypes.func.isRequired, setIsAddHostHidden: PropTypes.func.isRequired,
deleteHost: PropTypes.func.isRequired, deleteHost: PropTypes.func.isRequired,
editHost: PropTypes.func.isRequired, editHost: PropTypes.func.isRequired,
openEditPanel: PropTypes.func.isRequired,
}; };
export default HostViewer; export default HostViewer;

View File

@@ -28,7 +28,8 @@ const hostSchema = new mongoose.Schema({
name: { type: String, required: true }, name: { type: String, required: true },
config: { type: String, required: true }, config: { type: String, required: true },
users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], 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); const User = mongoose.model('User', userSchema);
@@ -178,7 +179,8 @@ io.of('/database.io').on('connection', (socket) => {
} }
const cleanConfig = { const cleanConfig = {
name: hostConfig.name.trim(), name: hostConfig.name?.trim(),
folder: hostConfig.folder?.trim() || null,
ip: hostConfig.ip.trim(), ip: hostConfig.ip.trim(),
user: hostConfig.user.trim(), user: hostConfig.user.trim(),
port: hostConfig.port || 22, port: hostConfig.port || 22,
@@ -208,7 +210,8 @@ io.of('/database.io').on('connection', (socket) => {
name: finalName, name: finalName,
config: encryptedConfig, config: encryptedConfig,
users: [userId], users: [userId],
createdBy: userId createdBy: userId,
folder: cleanConfig.folder
}); });
logger.info(`Host created successfully: ${finalName}`); logger.info(`Host created successfully: ${finalName}`);
@@ -360,10 +363,11 @@ io.of('/database.io').on('connection', (socket) => {
} }
const cleanConfig = { const cleanConfig = {
name: newHostConfig.name?.trim(),
folder: newHostConfig.folder?.trim() || null,
ip: newHostConfig.ip.trim(), ip: newHostConfig.ip.trim(),
user: newHostConfig.user.trim(), user: newHostConfig.user.trim(),
port: newHostConfig.port || 22, port: newHostConfig.port || 22,
name: newHostConfig.name.trim(),
password: newHostConfig.password?.trim() || undefined, password: newHostConfig.password?.trim() || undefined,
rsaKey: newHostConfig.rsaKey?.trim() || undefined rsaKey: newHostConfig.rsaKey?.trim() || undefined
}; };
@@ -375,6 +379,7 @@ io.of('/database.io').on('connection', (socket) => {
} }
host.config = encryptedConfig; host.config = encryptedConfig;
host.folder = cleanConfig.folder;
await host.save(); await host.save();
logger.info(`Host edited successfully`); logger.info(`Host edited successfully`);

View File

@@ -13,7 +13,11 @@ import {
Select, Select,
Option, Option,
Checkbox, Checkbox,
IconButton IconButton,
Tabs,
TabList,
Tab,
TabPanel
} from '@mui/joy'; } from '@mui/joy';
import theme from '/src/theme'; import theme from '/src/theme';
import { useState } from 'react'; import { useState } from 'react';
@@ -22,6 +26,7 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => { const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [activeTab, setActiveTab] = useState(0);
const handleFileChange = (e) => { const handleFileChange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
@@ -68,7 +73,6 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
<CssVarsProvider theme={theme}> <CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsAddHostHidden(true)} <Modal open={!isHidden} onClose={() => setIsAddHostHidden(true)}
sx={{ sx={{
overflow: 'hidden',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
@@ -82,17 +86,57 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: 3, padding: 3,
borderRadius: 10, borderRadius: 10,
maxWidth: '400px', maxWidth: '500px',
width: '100%', width: '100%',
overflow: 'hidden', maxHeight: '80vh',
overflow: 'auto',
boxSizing: 'border-box', boxSizing: 'border-box',
mx: 2, mx: 2,
}} }}
> >
<DialogTitle>Add Host</DialogTitle> <DialogTitle sx={{ mb: 2 }}>Add Host</DialogTitle>
<DialogContent> <DialogContent>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<Stack spacing={2} sx={{ width: '100%' }}> <Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}
sx={{
backgroundColor: theme.palette.general.disabled,
borderRadius: '8px',
padding: '8px',
marginBottom: '16px',
width: '100%',
}}
>
<TabList
sx={{
width: '100%',
gap: 0,
mb: 2,
'& button': {
flex: 1,
bgcolor: 'transparent',
color: theme.palette.text.secondary,
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.1)',
},
'&.Mui-selected': {
bgcolor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.primary,
},
},
},
}}
>
<Tab>Basic Info</Tab>
<Tab>Connection</Tab>
<Tab>Authentication</Tab>
</TabList>
<TabPanel value={0}>
<Stack spacing={2}>
<FormControl> <FormControl>
<FormLabel>Host Name</FormLabel> <FormLabel>Host Name</FormLabel>
<Input <Input
@@ -104,6 +148,22 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
}} }}
/> />
</FormControl> </FormControl>
<FormControl>
<FormLabel>Folder</FormLabel>
<Input
value={form.folder || ''}
onChange={(e) => setForm({ ...form, folder: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={1}>
<Stack spacing={2}>
<FormControl error={!form.ip}> <FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel> <FormLabel>Host IP</FormLabel>
<Input <Input
@@ -128,6 +188,54 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
}} }}
/> />
</FormControl> </FormControl>
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
type="number"
value={form.port}
onChange={(e) => setForm({ ...form, port: e.target.value })}
min={1}
max={65535}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={2}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Remember Host</FormLabel>
<Checkbox
checked={form.rememberHost}
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
{form.rememberHost && (
<>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}> <FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel> <FormLabel>Authentication Method</FormLabel>
<Select <Select
@@ -179,75 +287,57 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
{form.authMethod === 'rsaKey' && ( {form.authMethod === 'rsaKey' && (
<FormControl error={!form.rsaKey}> <FormControl error={!form.rsaKey}>
<FormLabel>RSA Key</FormLabel> <FormLabel>RSA Key</FormLabel>
<Input
type="file"
onChange={handleFileChange}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
padding: 1,
textAlign: 'center',
width: '100%',
}}
/>
</FormControl>
)}
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
value={form.port}
onChange={(e) => setForm({ ...form, port: e.target.value })}
min={1}
max={65535}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Remember Host</FormLabel>
<Checkbox
checked={form.rememberHost}
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
{form.rememberHost && (
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
)}
<Button <Button
type="submit" component="label"
disabled={!isFormValid()}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': { '&:hover': {
backgroundColor: theme.palette.general.disabled, backgroundColor: theme.palette.general.disabled,
}, },
}} }}
>
{form.rsaKey ? 'Change RSA Key File' : 'Upload RSA Key File'}
<Input
type="file"
onChange={handleFileChange}
required
sx={{ display: 'none' }}
/>
</Button>
</FormControl>
)}
</>
)}
</Stack>
</TabPanel>
</Tabs>
<Button
type="submit"
disabled={!isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
'&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)',
},
marginTop: 3,
width: '100%',
height: '40px',
}}
> >
Add Host Add Host
</Button> </Button>
</Stack>
</form> </form>
</DialogContent> </DialogContent>
</ModalDialog> </ModalDialog>
@@ -260,6 +350,7 @@ AddHostModal.propTypes = {
isHidden: PropTypes.bool.isRequired, isHidden: PropTypes.bool.isRequired,
form: PropTypes.shape({ form: PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
folder: PropTypes.string,
ip: PropTypes.string.isRequired, ip: PropTypes.string.isRequired,
user: PropTypes.string.isRequired, user: PropTypes.string.isRequired,
password: PropTypes.string, password: PropTypes.string,

View File

@@ -14,7 +14,11 @@ import {
Select, Select,
Option, Option,
IconButton, IconButton,
Checkbox Checkbox,
Tabs,
TabList,
Tab,
TabPanel
} from '@mui/joy'; } from '@mui/joy';
import theme from '/src/theme'; import theme from '/src/theme';
import Visibility from '@mui/icons-material/Visibility'; 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 EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [activeTab, setActiveTab] = useState(0);
useEffect(() => { useEffect(() => {
if (hostConfig) { if (hostConfig && !isHidden) {
const storePassword = hostConfig.password || hostConfig.rsaKey;
setForm({ setForm({
...form, name: hostConfig.name || "",
name: hostConfig.name || '', folder: hostConfig.folder || "",
ip: hostConfig.ip || '', ip: hostConfig.ip || "",
user: hostConfig.user || '', user: hostConfig.user || "",
password: storePassword && hostConfig.password ? hostConfig.password : '', password: hostConfig.password || "",
rsaKey: '', port: hostConfig.port || 22,
port: Number(hostConfig.port) || 22, authMethod: hostConfig.password ? "password" : hostConfig.rsaKey ? "rsaKey" : "Select Auth",
authMethod: hostConfig.rsaKey ? 'rsaKey' : (storePassword ? 'password' : 'Select Auth'), rememberHost: true,
rememberHost: hostConfig.rememberHost || true, storePassword: true,
storePassword: storePassword ?? false
}); });
} }
}, [hostConfig, setForm]); }, [hostConfig, isHidden]);
const handleFileChange = (e) => { const handleFileChange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
@@ -65,7 +67,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
const handleStorePasswordChange = (checked) => { const handleStorePasswordChange = (checked) => {
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
storePassword: checked, storePassword: Boolean(checked),
authMethod: checked ? 'password' : 'Select Auth' authMethod: checked ? 'password' : 'Select Auth'
})); }));
}; };
@@ -76,31 +78,38 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
const portNum = Number(port); const portNum = Number(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false; if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
if (storePassword && authMethod === 'password' && !password.trim()) return false; if (Boolean(storePassword) && authMethod === 'password' && !password?.trim()) return false;
if (storePassword && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false; if (Boolean(storePassword) && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false;
if (storePassword && authMethod === 'Select Auth') return false; if (Boolean(storePassword) && authMethod === 'Select Auth') return false;
return true; return true;
}; };
const handleSubmit = (e) => { const handleSave = async (e) => {
e.preventDefault(); e.preventDefault();
if (isFormValid()) { try {
const { authMethod, password, rsaKey, storePassword, ...rest } = form; const newConfig = {
handleEditHost({ ...form,
...rest, port: String(form.port),
authMethod, };
password: authMethod === 'password' && storePassword ? password : '',
rsaKey: authMethod === 'rsaKey' ? rsaKey : '' if (form.authMethod === 'rsaKey' || !form.storePassword) {
}); newConfig.password = '';
}
await handleEditHost(hostConfig, newConfig);
setIsEditHostHidden(true);
} catch (error) {
console.error('Failed to save:', error);
} }
}; };
return ( return (
<CssVarsProvider theme={theme}> <CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsEditHostHidden(true)} <Modal
open={!isHidden}
onClose={() => setIsEditHostHidden(true)}
sx={{ sx={{
overflowX: 'hidden',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
@@ -114,17 +123,57 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: 3, padding: 3,
borderRadius: 10, borderRadius: 10,
maxWidth: '400px', maxWidth: '500px',
width: '100%', width: '100%',
overflow: 'hidden', maxHeight: '80vh',
overflow: 'auto',
boxSizing: 'border-box', boxSizing: 'border-box',
mx: 2, mx: 2,
}} }}
> >
<DialogTitle>Edit Host</DialogTitle> <DialogTitle sx={{ mb: 2 }}>Edit Host</DialogTitle>
<DialogContent> <DialogContent>
<form onSubmit={handleSubmit}> <form onSubmit={handleSave}>
<Stack spacing={2} sx={{ width: '100%', overflow: 'hidden' }}> <Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}
sx={{
backgroundColor: theme.palette.general.disabled,
borderRadius: '8px',
padding: '8px',
marginBottom: '16px',
width: '100%',
}}
>
<TabList
sx={{
width: '100%',
gap: 0,
mb: 2,
'& button': {
flex: 1,
bgcolor: 'transparent',
color: theme.palette.text.secondary,
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.1)',
},
'&.Mui-selected': {
bgcolor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.primary,
},
},
},
}}
>
<Tab>Basic Info</Tab>
<Tab>Connection</Tab>
<Tab>Authentication</Tab>
</TabList>
<TabPanel value={0}>
<Stack spacing={2}>
<FormControl> <FormControl>
<FormLabel>Host Name</FormLabel> <FormLabel>Host Name</FormLabel>
<Input <Input
@@ -137,6 +186,22 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
/> />
</FormControl> </FormControl>
<FormControl>
<FormLabel>Folder</FormLabel>
<Input
value={form.folder}
onChange={(e) => setForm((prev) => ({ ...prev, folder: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={1}>
<Stack spacing={2}>
<FormControl error={!form.ip}> <FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel> <FormLabel>Host IP</FormLabel>
<Input <Input
@@ -149,6 +214,19 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
/> />
</FormControl> </FormControl>
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
type="number"
value={form.port}
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
<FormControl error={!form.user}> <FormControl error={!form.user}>
<FormLabel>Host User</FormLabel> <FormLabel>Host User</FormLabel>
<Input <Input
@@ -160,22 +238,34 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
}} }}
/> />
</FormControl> </FormControl>
</Stack>
</TabPanel>
{form.storePassword && form.authMethod !== 'Select Auth' && ( <TabPanel value={2}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => handleStorePasswordChange(e.target.checked)}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary
}
}}
/>
</FormControl>
{form.storePassword && (
<FormControl error={form.authMethod === 'Select Auth'}> <FormControl error={form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel> <FormLabel>Authentication Method</FormLabel>
<Select <Select
value={form.authMethod} value={form.authMethod}
onChange={(e, val) => handleAuthChange(val)} onChange={(e, val) => handleAuthChange(val)}
sx={{ sx={{
backgroundColor: backgroundColor: theme.palette.general.primary,
form.authMethod === 'Select Auth'
? theme.palette.general.tertiary
: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled
}
}} }}
> >
<Option value="Select Auth" disabled>Select Auth</Option> <Option value="Select Auth" disabled>Select Auth</Option>
@@ -192,9 +282,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
<Input <Input
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={form.password} value={form.password}
onChange={(e) => onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
setForm((prev) => ({ ...prev, password: e.target.value }))
}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
@@ -215,53 +303,48 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
)} )}
{form.authMethod === 'rsaKey' && form.storePassword && ( {form.authMethod === 'rsaKey' && form.storePassword && (
<FormControl <FormControl error={!form.rsaKey && !hostConfig?.rsaKey}>
error={!form.rsaKey && !hostConfig?.rsaKey}
sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
>
<FormLabel>RSA Key</FormLabel> <FormLabel>RSA Key</FormLabel>
<Input <Button
type="file" component="label"
onChange={handleFileChange}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
alignItems: 'center' width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}} }}
>
{form.rsaKey ? 'Change RSA Key File' : 'Upload RSA Key File'}
<Input
type="file"
onChange={handleFileChange}
sx={{ display: 'none' }}
/> />
</Button>
{hostConfig?.rsaKey && !form.rsaKey && ( {hostConfig?.rsaKey && !form.rsaKey && (
<FormLabel sx={{ color: theme.palette.text.secondary }}> <FormLabel
sx={{
color: theme.palette.text.secondary,
fontSize: '0.875rem',
mt: 1,
display: 'block',
textAlign: 'center'
}}
>
Existing key detected. Upload to replace. Existing key detected. Upload to replace.
</FormLabel> </FormLabel>
)} )}
</FormControl> </FormControl>
)} )}
</Stack>
<FormControl error={form.port < 1 || form.port > 65535}> </TabPanel>
<FormLabel>Host Port</FormLabel> </Tabs>
<Input
value={form.port}
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => handleStorePasswordChange(e.target.checked)}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary
}
}}
/>
</FormControl>
<Button <Button
type="submit" type="submit"
@@ -271,12 +354,18 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
color: theme.palette.text.primary, color: theme.palette.text.primary,
'&:hover': { '&:hover': {
backgroundColor: theme.palette.general.disabled backgroundColor: theme.palette.general.disabled
} },
'&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)',
},
marginTop: 3,
width: '100%',
height: '40px',
}} }}
> >
Save Changes Save Changes
</Button> </Button>
</Stack>
</form> </form>
</DialogContent> </DialogContent>
</ModalDialog> </ModalDialog>

View File

@@ -1,101 +1,83 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CssVarsProvider } from '@mui/joy/styles'; import { Modal, Typography, Button } from "@mui/joy";
import { Modal, Button, DialogTitle, DialogContent, ModalDialog, Stack } from '@mui/joy'; import LogoutIcon from "@mui/icons-material/Logout";
import theme from '/src/theme'; import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
const ProfileModal = ({ isHidden, getUser, handleDeleteUser, handleLogoutUser, setIsProfileHidden }) => { import theme from "../theme";
const handleDelete = () => {
handleDeleteUser({
onSuccess: () => {
window.location.reload();
}
});
};
const handleLogout = () => {
handleLogoutUser({
onSuccess: () => {
window.location.reload();
}
});
}
const getUserName = () => {
const user = getUser();
return user ? user.username : '';
}
export default function ProfileModal({
isHidden,
getUser,
handleDeleteUser,
handleLogoutUser,
setIsProfileHidden,
}) {
return ( return (
<CssVarsProvider theme={theme}> <Modal
<Modal open={!isHidden} onClose={() => setIsProfileHidden(true)}> open={!isHidden}
<ModalDialog onClose={() => setIsProfileHidden(true)}
layout="center"
sx={{ sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<div style={{
backgroundColor: theme.palette.general.tertiary, backgroundColor: theme.palette.general.tertiary,
borderColor: theme.palette.general.secondary, borderColor: theme.palette.general.secondary,
color: theme.palette.text.primary, borderWidth: "1px",
padding: 3, borderStyle: "solid",
borderRadius: 10, borderRadius: "0.5rem",
width: "auto", width: "400px",
maxWidth: "90vw",
minWidth: "fit-content",
overflow: "hidden", overflow: "hidden",
display: "flex", }}>
flexDirection: "column", <div className="p-4 flex flex-col gap-4">
alignItems: "center",
justifyContent: "center",
gap: 1,
}}
>
<DialogTitle
sx={{
marginBottom: 1.5,
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
padding: 1,
borderRadius: 10,
width: "100%",
textAlign: "center",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
User: {getUserName()}
</DialogTitle>
<DialogContent sx={{ width: "100%" }}>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden", mt: 1.5 }}>
<Button <Button
onClick={handleDelete} fullWidth
onClick={handleLogoutUser}
startDecorator={<LogoutIcon />}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.tertiary,
'&:hover': { color: "white",
backgroundColor: theme.palette.general.disabled, "&:hover": {
backgroundColor: theme.palette.general.secondary,
}, },
width: "100%", height: "40px",
}} border: `1px solid ${theme.palette.general.secondary}`,
>
Delete User
</Button>
<Button
onClick={handleLogout}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
width: "100%",
}} }}
> >
Logout Logout
</Button> </Button>
</Stack>
</DialogContent> <Button
</ModalDialog> fullWidth
color="danger"
onClick={() => {
if (window.confirm("Are you sure you want to delete your account? This action cannot be undone.")) {
handleDeleteUser({
onSuccess: () => setIsProfileHidden(true),
onFailure: (error) => console.error(error),
});
}
}}
startDecorator={<DeleteForeverIcon />}
sx={{
backgroundColor: "#c53030",
color: "white",
"&:hover": {
backgroundColor: "#9b2c2c",
},
height: "40px",
border: "1px solid #9b2c2c",
}}
>
Delete Account
</Button>
</div>
</div>
</Modal> </Modal>
</CssVarsProvider>
); );
}; }
ProfileModal.propTypes = { ProfileModal.propTypes = {
isHidden: PropTypes.bool.isRequired, isHidden: PropTypes.bool.isRequired,
@@ -104,5 +86,3 @@ ProfileModal.propTypes = {
handleLogoutUser: PropTypes.func.isRequired, handleLogoutUser: PropTypes.func.isRequired,
setIsProfileHidden: PropTypes.func.isRequired, setIsProfileHidden: PropTypes.func.isRequired,
}; };
export default ProfileModal;