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

View File

@@ -31,6 +31,7 @@ function App() {
const [nextId, setNextId] = useState(1);
const [addHostForm, setAddHostForm] = useState({
name: "",
folder: "",
ip: "",
user: "",
password: "",
@@ -41,6 +42,7 @@ function App() {
});
const [editHostForm, setEditHostForm] = useState({
name: "",
folder: "",
ip: "",
user: "",
password: "",
@@ -66,6 +68,7 @@ function App() {
const [splitTabIds, setSplitTabIds] = useState([]);
const [isEditHostHidden, setIsEditHostHidden] = useState(true);
const [currentHostConfig, setCurrentHostConfig] = useState(null);
const [isLoggingIn, setIsLoggingIn] = useState(true);
useEffect(() => {
const handleKeyDown = (e) => {
@@ -124,23 +127,97 @@ function App() {
useEffect(() => {
const sessionToken = localStorage.getItem('sessionToken');
if (sessionToken) {
setTimeout(() => {
handleLoginUser({
let isComponentMounted = true;
let isLoginInProgress = false;
if (userRef.current?.getUser()) {
setIsLoggingIn(false);
setIsLoginUserHidden(true);
return;
}
if (!sessionToken) {
setIsLoggingIn(false);
setIsLoginUserHidden(false);
return;
}
setIsLoggingIn(true);
let loginAttempts = 0;
const maxAttempts = 50;
let attemptLoginInterval;
const loginTimeout = setTimeout(() => {
if (isComponentMounted) {
clearInterval(attemptLoginInterval);
if (!userRef.current?.getUser()) {
localStorage.removeItem('sessionToken');
setIsLoginUserHidden(false);
setIsLoggingIn(false);
setErrorMessage('Login timed out. Please try again.');
setIsErrorHidden(false);
}
}
}, 10000);
const attemptLogin = () => {
if (!isComponentMounted || isLoginInProgress) return;
if (loginAttempts >= maxAttempts || userRef.current?.getUser()) {
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
if (!userRef.current?.getUser()) {
localStorage.removeItem('sessionToken');
setIsLoginUserHidden(false);
setIsLoggingIn(false);
setErrorMessage('Login timed out. Please try again.');
setIsErrorHidden(false);
}
return;
}
if (userRef.current) {
isLoginInProgress = true;
userRef.current.loginUser({
sessionToken,
onSuccess: () => {
setIsLoginUserHidden(true);
if (isComponentMounted) {
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
setIsLoginUserHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
}
isLoginInProgress = false;
},
onFailure: (error) => {
setErrorMessage(`Auto-login failed: ${error}`);
setIsErrorHidden(false);
setIsLoginUserHidden(false);
if (isComponentMounted) {
if (!userRef.current?.getUser()) {
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
localStorage.removeItem('sessionToken');
setErrorMessage(`Auto-login failed: ${error}`);
setIsErrorHidden(false);
setIsLoginUserHidden(false);
setIsLoggingIn(false);
}
}
isLoginInProgress = false;
},
});
}, 500);
} else {
setIsLoginUserHidden(false);
}
}
loginAttempts++;
};
attemptLoginInterval = setInterval(attemptLogin, 100);
attemptLogin();
return () => {
isComponentMounted = false;
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
};
}, []);
const handleAddHost = () => {
@@ -168,6 +245,8 @@ function App() {
id: nextId,
title: addHostForm.name || addHostForm.ip,
hostConfig: {
name: addHostForm.name,
folder: addHostForm.folder,
ip: addHostForm.ip,
user: addHostForm.user,
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
@@ -180,7 +259,7 @@ function App() {
setActiveTab(nextId);
setNextId(nextId + 1);
setIsAddHostHidden(true);
setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" });
setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth", rememberHost: false, storePassword: true });
}
const handleAuthSubmit = (form) => {
@@ -217,6 +296,7 @@ function App() {
const handleSaveHost = () => {
let hostConfig = {
name: addHostForm.name || addHostForm.ip,
folder: addHostForm.folder,
ip: addHostForm.ip,
user: addHostForm.user,
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
@@ -235,15 +315,32 @@ function App() {
if (sessionToken) {
userRef.current.loginUser({
sessionToken,
onSuccess,
onFailure,
onSuccess: () => {
setIsLoginUserHidden(true);
setIsLoggingIn(false);
if (onSuccess) onSuccess();
},
onFailure: (error) => {
localStorage.removeItem('sessionToken');
setIsLoginUserHidden(false);
setIsLoggingIn(false);
if (onFailure) onFailure(error);
},
});
} else {
userRef.current.loginUser({
username,
password,
onSuccess,
onFailure,
onSuccess: () => {
setIsLoginUserHidden(true);
setIsLoggingIn(false);
if (onSuccess) onSuccess();
},
onFailure: (error) => {
setIsLoginUserHidden(false);
setIsLoggingIn(false);
if (onFailure) onFailure(error);
},
});
}
}
@@ -264,7 +361,7 @@ function App() {
onFailure,
});
}
}
};
const handleDeleteUser = ({ onSuccess, onFailure }) => {
if (userRef.current) {
@@ -311,31 +408,21 @@ function App() {
}
};
const handleEditHost = async () => {
const handleEditHost = async (oldConfig, newConfig = null) => {
try {
// Only clear the password if switching to RSA or storePassword is false
if (editHostForm.authMethod === 'rsaKey') {
editHostForm.password = '';
} else if (!editHostForm.storePassword) {
editHostForm.password = '';
if (newConfig) {
await userRef.current.editHost({
oldHostConfig: oldConfig,
newHostConfig: newConfig,
});
return;
}
await userRef.current.editHost({
oldHostConfig: currentHostConfig,
newHostConfig: editHostForm,
});
// Refresh the updated config
const refreshedHosts = await userRef.current.getAllHosts();
const updated = refreshedHosts.find(
(h) => h.config.ip === editHostForm.ip && h.config.user === editHostForm.user
);
if (updated) {
setCurrentHostConfig(updated.config);
}
setIsEditHostHidden(true);
updateEditHostForm(oldConfig);
} catch (error) {
alert('Edit failed: ' + error);
console.error('Edit failed:', error);
setErrorMessage(`Edit failed: ${error}`);
setIsErrorHidden(false);
}
};
@@ -398,88 +485,124 @@ function App() {
</div>
</div>
{/* Launchpad Button */}
<Button
onClick={() => setIsLaunchpadOpen(true)}
sx={{
backgroundColor: theme.palette.general.tertiary,
"&:hover": { backgroundColor: theme.palette.general.secondary },
flexShrink: 0,
height: "52px",
width: "52px",
padding: 0,
}}
>
<img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70", objectFit: "contain" }} />
</Button>
{/* Action Buttons */}
<div className="flex gap-4">
{/* Launchpad Button */}
<Button
disabled={isLoggingIn || !userRef.current?.getUser()}
onClick={() => setIsLaunchpadOpen(true)}
sx={{
backgroundColor: theme.palette.general.tertiary,
"&:hover": { backgroundColor: theme.palette.general.secondary },
flexShrink: 0,
height: "52px",
width: "52px",
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" }} />
</Button>
{/* Add Host Button */}
<Button
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>
{/* Add Host Button */}
<Button
disabled={isLoggingIn || !userRef.current?.getUser()}
onClick={() => setIsAddHostHidden(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: (!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
onClick={() => setIsProfileHidden(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,
}}
>
<img
src={ProfileIcon}
alt="Profile"
style={{ width: "70%", height: "70%", objectFit: "contain" }}
/>
</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
src={ProfileIcon}
alt="Profile"
style={{ width: "70%", height: "70%", objectFit: "contain" }}
/>
</Button>
</div>
</div>
{/* Terminal Views */}
<div className={`relative p-4 terminal-container ${getLayoutStyle()}`}>
{terminals.map((terminal) => (
<div
key={terminal.id}
className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${
splitTabIds.includes(terminal.id) || activeTab === terminal.id ? "block" : "hidden"
} flex-1`}
style={{
order: splitTabIds.includes(terminal.id)
? splitTabIds.indexOf(terminal.id)
: 0,
}}
>
<NewTerminal
{userRef.current?.getUser() ? (
terminals.map((terminal) => (
<div
key={terminal.id}
hostConfig={terminal.hostConfig}
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
setIsNoAuthHidden={setIsNoAuthHidden}
ref={(ref) => {
terminal.terminalRef = ref;
className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${
splitTabIds.includes(terminal.id) || activeTab === terminal.id ? "block" : "hidden"
} flex-1`}
style={{
order: splitTabIds.includes(terminal.id)
? splitTabIds.indexOf(terminal.id)
: 0,
}}
/>
>
<NewTerminal
key={terminal.id}
hostConfig={terminal.hostConfig}
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
setIsNoAuthHidden={setIsNoAuthHidden}
ref={(ref) => {
terminal.terminalRef = ref;
}}
/>
</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
isHidden={isNoAuthHidden}
form={authForm}
@@ -488,85 +611,108 @@ function App() {
handleAuthSubmit={handleAuthSubmit}
/>
</div>
</div>
{/* Modals */}
<AddHostModal
isHidden={isAddHostHidden}
form={addHostForm}
setForm={setAddHostForm}
handleAddHost={handleAddHost}
setIsAddHostHidden={setIsAddHostHidden}
/>
<EditHostModal
isHidden={isEditHostHidden}
form={editHostForm}
setForm={setEditHostForm}
handleEditHost={handleEditHost}
setIsEditHostHidden={setIsEditHostHidden}
hostConfig={currentHostConfig}
/>
<CreateUserModal
isHidden={isCreateUserHidden}
form={createUserForm}
setForm={setCreateUserForm}
handleCreateUser={handleCreateUser}
setIsCreateUserHidden={setIsCreateUserHidden}
setIsLoginUserHidden={setIsLoginUserHidden}
/>
<ProfileModal
isHidden={isProfileHidden}
getUser={getUser}
handleDeleteUser={handleDeleteUser}
handleLogoutUser={handleLogoutUser}
setIsProfileHidden={setIsProfileHidden}
/>
<ErrorModal
isHidden={isErrorHidden}
errorMessage={errorMessage}
setIsErrorHidden={setIsErrorHidden}
/>
{isLaunchpadOpen && (
<Launchpad
onClose={() => setIsLaunchpadOpen(false)}
getHosts={getHosts}
connectToHost={connectToHostWithConfig}
isAddHostHidden={isAddHostHidden}
setIsAddHostHidden={setIsAddHostHidden}
isEditHostHidden={isEditHostHidden}
isErrorHidden={isErrorHidden}
deleteHost={deleteHost}
editHost={updateEditHostForm}
{/* Modals */}
{userRef.current?.getUser() && (
<>
<AddHostModal
isHidden={isAddHostHidden}
form={addHostForm}
setForm={setAddHostForm}
handleAddHost={handleAddHost}
setIsAddHostHidden={setIsAddHostHidden}
/>
<EditHostModal
isHidden={isEditHostHidden}
form={editHostForm}
setForm={setEditHostForm}
handleEditHost={handleEditHost}
setIsEditHostHidden={setIsEditHostHidden}
hostConfig={currentHostConfig}
/>
<ProfileModal
isHidden={isProfileHidden}
getUser={getUser}
handleDeleteUser={handleDeleteUser}
handleLogoutUser={handleLogoutUser}
setIsProfileHidden={setIsProfileHidden}
/>
{isLaunchpadOpen && (
<Launchpad
onClose={() => setIsLaunchpadOpen(false)}
getHosts={getHosts}
connectToHost={connectToHostWithConfig}
isAddHostHidden={isAddHostHidden}
setIsAddHostHidden={setIsAddHostHidden}
isEditHostHidden={isEditHostHidden}
isErrorHidden={isErrorHidden}
deleteHost={deleteHost}
editHost={handleEditHost}
/>
)}
</>
)}
<ErrorModal
isHidden={isErrorHidden}
errorMessage={errorMessage}
setIsErrorHidden={setIsErrorHidden}
/>
)}
<LoginUserModal
isHidden={isLoginUserHidden}
form={loginUserForm}
setForm={setLoginUserForm}
handleLoginUser={handleLoginUser}
handleGuestLogin={handleGuestLogin}
setIsLoginUserHidden={setIsLoginUserHidden}
setIsCreateUserHidden={setIsCreateUserHidden}
/>
<LoginUserModal
isHidden={isLoginUserHidden}
form={loginUserForm}
setForm={setLoginUserForm}
handleLoginUser={handleLoginUser}
handleGuestLogin={handleGuestLogin}
setIsLoginUserHidden={setIsLoginUserHidden}
setIsCreateUserHidden={setIsCreateUserHidden}
/>
{/* User component */}
<User
ref={userRef}
onLoginSuccess={() => setIsLoginUserHidden(true)}
onCreateSuccess={() => {
setIsCreateUserHidden(true);
handleLoginUser({ username: createUserForm.username, password: createUserForm.password })}
}
onDeleteSuccess={() => {
setIsProfileHidden(true);
window.location.reload();
}}
onFailure={(error) => {
setErrorMessage(`Action failed: ${error}`);
setIsErrorHidden(false);
}}
/>
<CreateUserModal
isHidden={isCreateUserHidden}
form={createUserForm}
setForm={setCreateUserForm}
handleCreateUser={handleCreateUser}
setIsCreateUserHidden={setIsCreateUserHidden}
setIsLoginUserHidden={setIsLoginUserHidden}
/>
{/* User component */}
<User
ref={userRef}
onLoginSuccess={() => {
setIsLoginUserHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
}}
onCreateSuccess={() => {
setIsCreateUserHidden(true);
handleLoginUser({
username: createUserForm.username,
password: createUserForm.password,
onSuccess: () => {
setIsLoginUserHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
},
onFailure: (error) => {
setErrorMessage(`Login failed: ${error}`);
setIsErrorHidden(false);
}
});
}}
onDeleteSuccess={() => {
setIsProfileHidden(true);
window.location.reload();
}}
onFailure={(error) => {
setErrorMessage(`Action failed: ${error}`);
setIsErrorHidden(false);
setIsLoggingIn(false);
}}
/>
</div>
</div>
</CssVarsProvider>
);

View File

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

View File

@@ -2,11 +2,14 @@ import PropTypes from "prop-types";
import { useState, useEffect, useRef } from "react";
import { Button, Input } from "@mui/joy";
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost }) {
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost, openEditPanel }) {
const [hosts, setHosts] = useState([]);
const [filteredHosts, setFilteredHosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [collapsedFolders, setCollapsedFolders] = useState(new Set());
const [draggedHost, setDraggedHost] = useState(null);
const [isDraggingOver, setIsDraggingOver] = useState(null);
const isMounted = useRef(true);
const fetchHosts = async () => {
@@ -44,11 +47,181 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
useEffect(() => {
const filtered = hosts.filter((hostWrapper) => {
const hostConfig = hostWrapper.config || {};
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) || hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase());
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
hostConfig.folder?.toLowerCase().includes(searchTerm.toLowerCase());
});
setFilteredHosts(filtered);
}, [searchTerm, hosts]);
const toggleFolder = (folderName) => {
setCollapsedFolders(prev => {
const newSet = new Set(prev);
if (newSet.has(folderName)) {
newSet.delete(folderName);
} else {
newSet.add(folderName);
}
return newSet;
});
};
const groupHostsByFolder = (hosts) => {
const grouped = {};
const noFolder = [];
const sortedHosts = [...hosts].sort((a, b) => {
const nameA = (a.config?.name || a.config?.ip || '').toLowerCase();
const nameB = (b.config?.name || b.config?.ip || '').toLowerCase();
return nameA.localeCompare(nameB);
});
sortedHosts.forEach(host => {
const folder = host.config?.folder;
if (folder) {
if (!grouped[folder]) {
grouped[folder] = [];
}
grouped[folder].push(host);
} else {
noFolder.push(host);
}
});
const sortedFolders = Object.keys(grouped).sort((a, b) => a.localeCompare(b));
return { grouped, sortedFolders, noFolder };
};
const handleDragStart = (e, host) => {
setDraggedHost(host);
e.dataTransfer.setData('text/plain', '');
};
const handleDragOver = (e, folderName) => {
e.preventDefault();
setIsDraggingOver(folderName);
};
const handleDragLeave = () => {
setIsDraggingOver(null);
};
const handleDrop = async (e, targetFolder) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingOver(null);
if (!draggedHost) return;
if (draggedHost.config.folder === targetFolder) return;
const newConfig = {
...draggedHost.config,
folder: targetFolder
};
try {
await editHost(draggedHost.config, newConfig);
await fetchHosts();
} catch (error) {
console.error('Failed to update folder:', error);
}
setDraggedHost(null);
};
const handleDropOnNoFolder = async (e) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingOver(null);
if (!draggedHost || !draggedHost.config.folder) return;
const newConfig = {
...draggedHost.config,
folder: null
};
try {
await editHost(draggedHost.config, newConfig);
await fetchHosts();
} catch (error) {
console.error('Failed to remove from folder:', error);
}
setDraggedHost(null);
};
const renderHostItem = (hostWrapper) => {
const hostConfig = hostWrapper.config || {};
if (!hostConfig) {
return null;
}
return (
<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 (
<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">
@@ -79,60 +252,51 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
<p className="text-gray-300">Loading hosts...</p>
) : filteredHosts.length > 0 ? (
<div className="flex flex-col gap-2 w-full">
{filteredHosts.map((hostWrapper, index) => {
const hostConfig = hostWrapper.config || {};
if (!hostConfig) {
return null;
}
{(() => {
const { grouped, sortedFolders, noFolder } = groupHostsByFolder(filteredHosts);
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>
<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>
<>
{/* Render hosts without folders first */}
<div
className={`flex flex-col gap-2 p-2 rounded-lg transition-colors ${isDraggingOver === 'no-folder' ? 'bg-neutral-700' : ''}`}
onDragOver={(e) => handleDragOver(e, 'no-folder')}
onDragLeave={handleDragLeave}
onDrop={handleDropOnNoFolder}
>
{noFolder.map((host) => renderHostItem(host))}
</div>
<div className="flex gap-2">
<Button
className="text-black"
onClick={() => connectToHost(hostConfig)}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Connect
</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>
{!collapsedFolders.has(folderName) && (
<div className="ml-6 mt-2 flex flex-col gap-2">
{grouped[folderName].map((host) => renderHostItem(host))}
</div>
)}
</div>
))}
</>
);
})}
})()}
</div>
) : (
<p className="text-gray-300">No hosts available...</p>
@@ -148,6 +312,7 @@ HostViewer.propTypes = {
setIsAddHostHidden: PropTypes.func.isRequired,
deleteHost: PropTypes.func.isRequired,
editHost: PropTypes.func.isRequired,
openEditPanel: PropTypes.func.isRequired,
};
export default HostViewer;

View File

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

View File

@@ -13,7 +13,11 @@ import {
Select,
Option,
Checkbox,
IconButton
IconButton,
Tabs,
TabList,
Tab,
TabPanel
} from '@mui/joy';
import theme from '/src/theme';
import { useState } from 'react';
@@ -22,6 +26,7 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
const [showPassword, setShowPassword] = useState(false);
const [activeTab, setActiveTab] = useState(0);
const handleFileChange = (e) => {
const file = e.target.files[0];
@@ -68,7 +73,6 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
<CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsAddHostHidden(true)}
sx={{
overflow: 'hidden',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
@@ -82,172 +86,258 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
color: theme.palette.text.primary,
padding: 3,
borderRadius: 10,
maxWidth: '400px',
maxWidth: '500px',
width: '100%',
overflow: 'hidden',
maxHeight: '80vh',
overflow: 'auto',
boxSizing: 'border-box',
mx: 2,
}}
>
<DialogTitle>Add Host</DialogTitle>
<DialogTitle sx={{ mb: 2 }}>Add Host</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmit}>
<Stack spacing={2} sx={{ width: '100%' }}>
<FormControl>
<FormLabel>Host Name</FormLabel>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel>
<Input
value={form.ip}
onChange={(e) => setForm({ ...form, ip: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl error={!form.user}>
<FormLabel>Host User</FormLabel>
<Input
value={form.user}
onChange={(e) => setForm({ ...form, user: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod || 'Select Auth'}
onChange={(e, newValue) => setForm({ ...form, authMethod: newValue })}
required
sx={{
backgroundColor: !form.authMethod || form.authMethod === 'Select Auth' ? theme.palette.general.tertiary : theme.palette.general.primary,
color: theme.palette.text.primary,
<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': {
backgroundColor: theme.palette.general.disabled,
bgcolor: 'rgba(255, 255, 255, 0.1)',
},
}}
>
<Option value="Select Auth" disabled>
Select Auth
</Option>
<Option value="password">Password</Option>
<Option value="rsaKey">RSA Key</Option>
</Select>
</FormControl>
{form.authMethod === 'password' && (
<FormControl error={!form.password}>
<FormLabel>Host Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
'&.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>
<FormLabel>Host Name</FormLabel>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</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}>
<FormLabel>Host IP</FormLabel>
<Input
value={form.ip}
onChange={(e) => setForm({ ...form, ip: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1,
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
</FormControl>
<FormControl error={!form.user}>
<FormLabel>Host User</FormLabel>
<Input
value={form.user}
onChange={(e) => setForm({ ...form, user: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</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,
marginLeft: 1,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'rsaKey' && (
<FormControl error={!form.rsaKey}>
<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
type="submit"
disabled={!isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
Add Host
</Button>
</Stack>
/>
</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'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod || 'Select Auth'}
onChange={(e, newValue) => setForm({ ...form, authMethod: newValue })}
required
sx={{
backgroundColor: !form.authMethod || form.authMethod === 'Select Auth' ? theme.palette.general.tertiary : theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
<Option value="Select Auth" disabled>
Select Auth
</Option>
<Option value="password">Password</Option>
<Option value="rsaKey">RSA Key</Option>
</Select>
</FormControl>
{form.authMethod === 'password' && (
<FormControl error={!form.password}>
<FormLabel>Host Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1,
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1,
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'rsaKey' && (
<FormControl error={!form.rsaKey}>
<FormLabel>RSA Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
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}
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
</Button>
</form>
</DialogContent>
</ModalDialog>
@@ -260,6 +350,7 @@ AddHostModal.propTypes = {
isHidden: PropTypes.bool.isRequired,
form: PropTypes.shape({
name: PropTypes.string,
folder: PropTypes.string,
ip: PropTypes.string.isRequired,
user: PropTypes.string.isRequired,
password: PropTypes.string,

View File

@@ -14,7 +14,11 @@ import {
Select,
Option,
IconButton,
Checkbox
Checkbox,
Tabs,
TabList,
Tab,
TabPanel
} from '@mui/joy';
import theme from '/src/theme';
import Visibility from '@mui/icons-material/Visibility';
@@ -22,25 +26,23 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => {
const [showPassword, setShowPassword] = useState(false);
const [activeTab, setActiveTab] = useState(0);
useEffect(() => {
if (hostConfig) {
const storePassword = hostConfig.password || hostConfig.rsaKey;
if (hostConfig && !isHidden) {
setForm({
...form,
name: hostConfig.name || '',
ip: hostConfig.ip || '',
user: hostConfig.user || '',
password: storePassword && hostConfig.password ? hostConfig.password : '',
rsaKey: '',
port: Number(hostConfig.port) || 22,
authMethod: hostConfig.rsaKey ? 'rsaKey' : (storePassword ? 'password' : 'Select Auth'),
rememberHost: hostConfig.rememberHost || true,
storePassword: storePassword ?? false
name: hostConfig.name || "",
folder: hostConfig.folder || "",
ip: hostConfig.ip || "",
user: hostConfig.user || "",
password: hostConfig.password || "",
port: hostConfig.port || 22,
authMethod: hostConfig.password ? "password" : hostConfig.rsaKey ? "rsaKey" : "Select Auth",
rememberHost: true,
storePassword: true,
});
}
}, [hostConfig, setForm]);
}, [hostConfig, isHidden]);
const handleFileChange = (e) => {
const file = e.target.files[0];
@@ -65,7 +67,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
const handleStorePasswordChange = (checked) => {
setForm((prev) => ({
...prev,
storePassword: checked,
storePassword: Boolean(checked),
authMethod: checked ? 'password' : 'Select Auth'
}));
};
@@ -76,35 +78,42 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
const portNum = Number(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
if (storePassword && authMethod === 'password' && !password.trim()) return false;
if (storePassword && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false;
if (storePassword && authMethod === 'Select Auth') return false;
if (Boolean(storePassword) && authMethod === 'password' && !password?.trim()) return false;
if (Boolean(storePassword) && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false;
if (Boolean(storePassword) && authMethod === 'Select Auth') return false;
return true;
};
const handleSubmit = (e) => {
const handleSave = async (e) => {
e.preventDefault();
if (isFormValid()) {
const { authMethod, password, rsaKey, storePassword, ...rest } = form;
handleEditHost({
...rest,
authMethod,
password: authMethod === 'password' && storePassword ? password : '',
rsaKey: authMethod === 'rsaKey' ? rsaKey : ''
});
try {
const newConfig = {
...form,
port: String(form.port),
};
if (form.authMethod === 'rsaKey' || !form.storePassword) {
newConfig.password = '';
}
await handleEditHost(hostConfig, newConfig);
setIsEditHostHidden(true);
} catch (error) {
console.error('Failed to save:', error);
}
};
return (
<CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsEditHostHidden(true)}
sx={{
overflowX: 'hidden',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
<Modal
open={!isHidden}
onClose={() => setIsEditHostHidden(true)}
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<ModalDialog
layout="center"
@@ -114,169 +123,249 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
color: theme.palette.text.primary,
padding: 3,
borderRadius: 10,
maxWidth: '400px',
maxWidth: '500px',
width: '100%',
overflow: 'hidden',
maxHeight: '80vh',
overflow: 'auto',
boxSizing: 'border-box',
mx: 2,
}}
>
<DialogTitle>Edit Host</DialogTitle>
<DialogTitle sx={{ mb: 2 }}>Edit Host</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmit}>
<Stack spacing={2} sx={{ width: '100%', overflow: 'hidden' }}>
<FormControl>
<FormLabel>Host Name</FormLabel>
<Input
value={form.name}
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
<FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel>
<Input
value={form.ip}
onChange={(e) => setForm((prev) => ({ ...prev, ip: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
<FormControl error={!form.user}>
<FormLabel>Host User</FormLabel>
<Input
value={form.user}
onChange={(e) => setForm((prev) => ({ ...prev, user: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
{form.storePassword && form.authMethod !== 'Select Auth' && (
<FormControl error={form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod}
onChange={(e, val) => handleAuthChange(val)}
sx={{
backgroundColor:
form.authMethod === 'Select Auth'
? theme.palette.general.tertiary
: theme.palette.general.primary,
<form onSubmit={handleSave}>
<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': {
backgroundColor: theme.palette.general.disabled
}
}}
>
<Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option>
<Option value="rsaKey">RSA Key</Option>
</Select>
</FormControl>
)}
{form.authMethod === 'password' && form.storePassword && (
<FormControl error={!form.password}>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) =>
setForm((prev) => ({ ...prev, password: e.target.value }))
}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'rsaKey' && form.storePassword && (
<FormControl
error={!form.rsaKey && !hostConfig?.rsaKey}
sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
>
<FormLabel>RSA Key</FormLabel>
<Input
type="file"
onChange={handleFileChange}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
alignItems: 'center'
}}
/>
{hostConfig?.rsaKey && !form.rsaKey && (
<FormLabel sx={{ color: theme.palette.text.secondary }}>
Existing key detected. Upload to replace.
</FormLabel>
)}
</FormControl>
)}
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<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
type="submit"
disabled={!isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled
}
bgcolor: theme.palette.general.primary,
},
},
},
}}
>
Save Changes
</Button>
</Stack>
<Tab>Basic Info</Tab>
<Tab>Connection</Tab>
<Tab>Authentication</Tab>
</TabList>
<TabPanel value={0}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Host Name</FormLabel>
<Input
value={form.name}
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</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}>
<FormLabel>Host IP</FormLabel>
<Input
value={form.ip}
onChange={(e) => setForm((prev) => ({ ...prev, ip: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</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}>
<FormLabel>Host User</FormLabel>
<Input
value={form.user}
onChange={(e) => setForm((prev) => ({ ...prev, user: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
</Stack>
</TabPanel>
<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'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod}
onChange={(e, val) => handleAuthChange(val)}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
>
<Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option>
<Option value="rsaKey">RSA Key</Option>
</Select>
</FormControl>
)}
{form.authMethod === 'password' && form.storePassword && (
<FormControl error={!form.password}>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'rsaKey' && form.storePassword && (
<FormControl error={!form.rsaKey && !hostConfig?.rsaKey}>
<FormLabel>RSA Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
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 && (
<FormLabel
sx={{
color: theme.palette.text.secondary,
fontSize: '0.875rem',
mt: 1,
display: 'block',
textAlign: 'center'
}}
>
Existing key detected. Upload to replace.
</FormLabel>
)}
</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',
}}
>
Save Changes
</Button>
</form>
</DialogContent>
</ModalDialog>

View File

@@ -1,101 +1,83 @@
import PropTypes from 'prop-types';
import { CssVarsProvider } from '@mui/joy/styles';
import { Modal, Button, DialogTitle, DialogContent, ModalDialog, Stack } from '@mui/joy';
import theme from '/src/theme';
const ProfileModal = ({ isHidden, getUser, handleDeleteUser, handleLogoutUser, setIsProfileHidden }) => {
const handleDelete = () => {
handleDeleteUser({
onSuccess: () => {
window.location.reload();
}
});
};
const handleLogout = () => {
handleLogoutUser({
onSuccess: () => {
window.location.reload();
}
});
}
const getUserName = () => {
const user = getUser();
return user ? user.username : '';
}
import { Modal, Typography, Button } from "@mui/joy";
import LogoutIcon from "@mui/icons-material/Logout";
import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import theme from "../theme";
export default function ProfileModal({
isHidden,
getUser,
handleDeleteUser,
handleLogoutUser,
setIsProfileHidden,
}) {
return (
<CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsProfileHidden(true)}>
<ModalDialog
layout="center"
sx={{
backgroundColor: theme.palette.general.tertiary,
borderColor: theme.palette.general.secondary,
color: theme.palette.text.primary,
padding: 3,
borderRadius: 10,
width: "auto",
maxWidth: "90vw",
minWidth: "fit-content",
overflow: "hidden",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 1,
}}
>
<DialogTitle
<Modal
open={!isHidden}
onClose={() => setIsProfileHidden(true)}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<div style={{
backgroundColor: theme.palette.general.tertiary,
borderColor: theme.palette.general.secondary,
borderWidth: "1px",
borderStyle: "solid",
borderRadius: "0.5rem",
width: "400px",
overflow: "hidden",
}}>
<div className="p-4 flex flex-col gap-4">
<Button
fullWidth
onClick={handleLogoutUser}
startDecorator={<LogoutIcon />}
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",
backgroundColor: theme.palette.general.tertiary,
color: "white",
"&:hover": {
backgroundColor: theme.palette.general.secondary,
},
height: "40px",
border: `1px solid ${theme.palette.general.secondary}`,
}}
>
User: {getUserName()}
</DialogTitle>
<DialogContent sx={{ width: "100%" }}>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden", mt: 1.5 }}>
<Button
onClick={handleDelete}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
width: "100%",
}}
>
Delete User
</Button>
<Button
onClick={handleLogout}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
width: "100%",
}}
>
Logout
</Button>
</Stack>
</DialogContent>
</ModalDialog>
</Modal>
</CssVarsProvider>
Logout
</Button>
<Button
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>
);
};
}
ProfileModal.propTypes = {
isHidden: PropTypes.bool.isRequired,
@@ -103,6 +85,4 @@ ProfileModal.propTypes = {
handleDeleteUser: PropTypes.func.isRequired,
handleLogoutUser: PropTypes.func.isRequired,
setIsProfileHidden: PropTypes.func.isRequired,
};
export default ProfileModal;
};