Dev 2.0 #23

Merged
LukeGus merged 44 commits from dev-2.0 into main 2025-03-16 19:17:56 +00:00
7 changed files with 1162 additions and 692 deletions
Showing only changes of commit fda8c7ce4b - Show all commits
+217 -71
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>
); );
} }
+5 -11
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>
+215 -50
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;
+9 -4
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`);
+156 -65
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,
+172 -83
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>
+64 -84
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;