Waits for user to be able to log in. Improved UI for profile, edit and add host, and added organizational features to the host app (Folders, search, etc.)
This commit is contained in:
288
src/App.jsx
288
src/App.jsx
@@ -31,6 +31,7 @@ function App() {
|
||||
const [nextId, setNextId] = useState(1);
|
||||
const [addHostForm, setAddHostForm] = useState({
|
||||
name: "",
|
||||
folder: "",
|
||||
ip: "",
|
||||
user: "",
|
||||
password: "",
|
||||
@@ -41,6 +42,7 @@ function App() {
|
||||
});
|
||||
const [editHostForm, setEditHostForm] = useState({
|
||||
name: "",
|
||||
folder: "",
|
||||
ip: "",
|
||||
user: "",
|
||||
password: "",
|
||||
@@ -66,6 +68,7 @@ function App() {
|
||||
const [splitTabIds, setSplitTabIds] = useState([]);
|
||||
const [isEditHostHidden, setIsEditHostHidden] = useState(true);
|
||||
const [currentHostConfig, setCurrentHostConfig] = useState(null);
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
@@ -124,23 +127,97 @@ function App() {
|
||||
|
||||
useEffect(() => {
|
||||
const sessionToken = localStorage.getItem('sessionToken');
|
||||
if (sessionToken) {
|
||||
setTimeout(() => {
|
||||
handleLoginUser({
|
||||
let isComponentMounted = true;
|
||||
let isLoginInProgress = false;
|
||||
|
||||
if (userRef.current?.getUser()) {
|
||||
setIsLoggingIn(false);
|
||||
setIsLoginUserHidden(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessionToken) {
|
||||
setIsLoggingIn(false);
|
||||
setIsLoginUserHidden(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoggingIn(true);
|
||||
let loginAttempts = 0;
|
||||
const maxAttempts = 50;
|
||||
let attemptLoginInterval;
|
||||
|
||||
const loginTimeout = setTimeout(() => {
|
||||
if (isComponentMounted) {
|
||||
clearInterval(attemptLoginInterval);
|
||||
if (!userRef.current?.getUser()) {
|
||||
localStorage.removeItem('sessionToken');
|
||||
setIsLoginUserHidden(false);
|
||||
setIsLoggingIn(false);
|
||||
setErrorMessage('Login timed out. Please try again.');
|
||||
setIsErrorHidden(false);
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
const attemptLogin = () => {
|
||||
if (!isComponentMounted || isLoginInProgress) return;
|
||||
|
||||
if (loginAttempts >= maxAttempts || userRef.current?.getUser()) {
|
||||
clearTimeout(loginTimeout);
|
||||
clearInterval(attemptLoginInterval);
|
||||
|
||||
if (!userRef.current?.getUser()) {
|
||||
localStorage.removeItem('sessionToken');
|
||||
setIsLoginUserHidden(false);
|
||||
setIsLoggingIn(false);
|
||||
setErrorMessage('Login timed out. Please try again.');
|
||||
setIsErrorHidden(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (userRef.current) {
|
||||
isLoginInProgress = true;
|
||||
userRef.current.loginUser({
|
||||
sessionToken,
|
||||
onSuccess: () => {
|
||||
if (isComponentMounted) {
|
||||
clearTimeout(loginTimeout);
|
||||
clearInterval(attemptLoginInterval);
|
||||
setIsLoginUserHidden(true);
|
||||
setIsLoggingIn(false);
|
||||
setIsErrorHidden(true);
|
||||
}
|
||||
isLoginInProgress = false;
|
||||
},
|
||||
onFailure: (error) => {
|
||||
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: currentHostConfig,
|
||||
newHostConfig: editHostForm,
|
||||
oldHostConfig: oldConfig,
|
||||
newHostConfig: newConfig,
|
||||
});
|
||||
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
setIsEditHostHidden(true);
|
||||
|
||||
updateEditHostForm(oldConfig);
|
||||
} 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>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-4">
|
||||
{/* Launchpad Button */}
|
||||
<Button
|
||||
disabled={isLoggingIn || !userRef.current?.getUser()}
|
||||
onClick={() => setIsLaunchpadOpen(true)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.tertiary,
|
||||
@@ -408,33 +498,21 @@ function App() {
|
||||
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" }} />
|
||||
<img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70%", objectFit: "contain" }} />
|
||||
</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",
|
||||
fontSize: "3.5rem",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingTop: "2px",
|
||||
}}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
|
||||
{/* Profile Button */}
|
||||
<Button
|
||||
onClick={() => setIsProfileHidden(false)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.tertiary,
|
||||
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
||||
@@ -445,6 +523,41 @@ function App() {
|
||||
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
|
||||
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
|
||||
@@ -454,10 +567,12 @@ function App() {
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Terminal Views */}
|
||||
<div className={`relative p-4 terminal-container ${getLayoutStyle()}`}>
|
||||
{terminals.map((terminal) => (
|
||||
{userRef.current?.getUser() ? (
|
||||
terminals.map((terminal) => (
|
||||
<div
|
||||
key={terminal.id}
|
||||
className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${
|
||||
@@ -479,7 +594,15 @@ function App() {
|
||||
}}
|
||||
/>
|
||||
</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,9 +611,10 @@ function App() {
|
||||
handleAuthSubmit={handleAuthSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
{userRef.current?.getUser() && (
|
||||
<>
|
||||
<AddHostModal
|
||||
isHidden={isAddHostHidden}
|
||||
form={addHostForm}
|
||||
@@ -506,14 +630,6 @@ function App() {
|
||||
setIsEditHostHidden={setIsEditHostHidden}
|
||||
hostConfig={currentHostConfig}
|
||||
/>
|
||||
<CreateUserModal
|
||||
isHidden={isCreateUserHidden}
|
||||
form={createUserForm}
|
||||
setForm={setCreateUserForm}
|
||||
handleCreateUser={handleCreateUser}
|
||||
setIsCreateUserHidden={setIsCreateUserHidden}
|
||||
setIsLoginUserHidden={setIsLoginUserHidden}
|
||||
/>
|
||||
<ProfileModal
|
||||
isHidden={isProfileHidden}
|
||||
getUser={getUser}
|
||||
@@ -521,11 +637,6 @@ function App() {
|
||||
handleLogoutUser={handleLogoutUser}
|
||||
setIsProfileHidden={setIsProfileHidden}
|
||||
/>
|
||||
<ErrorModal
|
||||
isHidden={isErrorHidden}
|
||||
errorMessage={errorMessage}
|
||||
setIsErrorHidden={setIsErrorHidden}
|
||||
/>
|
||||
{isLaunchpadOpen && (
|
||||
<Launchpad
|
||||
onClose={() => setIsLaunchpadOpen(false)}
|
||||
@@ -536,9 +647,17 @@ function App() {
|
||||
isEditHostHidden={isEditHostHidden}
|
||||
isErrorHidden={isErrorHidden}
|
||||
deleteHost={deleteHost}
|
||||
editHost={updateEditHostForm}
|
||||
editHost={handleEditHost}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ErrorModal
|
||||
isHidden={isErrorHidden}
|
||||
errorMessage={errorMessage}
|
||||
setIsErrorHidden={setIsErrorHidden}
|
||||
/>
|
||||
|
||||
<LoginUserModal
|
||||
isHidden={isLoginUserHidden}
|
||||
@@ -550,14 +669,39 @@ function App() {
|
||||
setIsCreateUserHidden={setIsCreateUserHidden}
|
||||
/>
|
||||
|
||||
<CreateUserModal
|
||||
isHidden={isCreateUserHidden}
|
||||
form={createUserForm}
|
||||
setForm={setCreateUserForm}
|
||||
handleCreateUser={handleCreateUser}
|
||||
setIsCreateUserHidden={setIsCreateUserHidden}
|
||||
setIsLoginUserHidden={setIsLoginUserHidden}
|
||||
/>
|
||||
|
||||
{/* User component */}
|
||||
<User
|
||||
ref={userRef}
|
||||
onLoginSuccess={() => setIsLoginUserHidden(true)}
|
||||
onLoginSuccess={() => {
|
||||
setIsLoginUserHidden(true);
|
||||
setIsLoggingIn(false);
|
||||
setIsErrorHidden(true);
|
||||
}}
|
||||
onCreateSuccess={() => {
|
||||
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={() => {
|
||||
setIsProfileHidden(true);
|
||||
window.location.reload();
|
||||
@@ -565,9 +709,11 @@ function App() {
|
||||
onFailure={(error) => {
|
||||
setErrorMessage(`Action failed: ${error}`);
|
||||
setIsErrorHidden(false);
|
||||
setIsLoggingIn(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CssVarsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ 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,
|
||||
function Launchpad({
|
||||
onClose,
|
||||
getHosts,
|
||||
connectToHost,
|
||||
isAddHostHidden,
|
||||
@@ -17,7 +16,7 @@ function Launchpad({onClose,
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="text-black"
|
||||
onClick={() => connectToHost(hostConfig)}
|
||||
sx={{
|
||||
backgroundColor: "#6e6e6e",
|
||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
||||
}}
|
||||
<>
|
||||
{/* 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}
|
||||
>
|
||||
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>
|
||||
{noFolder.map((host) => renderHostItem(host))}
|
||||
</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;
|
||||
@@ -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`);
|
||||
|
||||
@@ -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,17 +86,57 @@ 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%' }}>
|
||||
<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>
|
||||
<FormLabel>Host Name</FormLabel>
|
||||
<Input
|
||||
@@ -104,6 +148,22 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
@@ -128,6 +188,54 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
||||
}}
|
||||
/>
|
||||
</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'}>
|
||||
<FormLabel>Authentication Method</FormLabel>
|
||||
<Select
|
||||
@@ -179,75 +287,57 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
||||
{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()}
|
||||
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>
|
||||
</Stack>
|
||||
</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,
|
||||
|
||||
@@ -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,31 +78,38 @@ 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)}
|
||||
<Modal
|
||||
open={!isHidden}
|
||||
onClose={() => setIsEditHostHidden(true)}
|
||||
sx={{
|
||||
overflowX: 'hidden',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
@@ -114,17 +123,57 @@ 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' }}>
|
||||
<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': {
|
||||
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
|
||||
@@ -137,6 +186,22 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
||||
/>
|
||||
</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
|
||||
@@ -149,6 +214,19 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
||||
/>
|
||||
</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
|
||||
@@ -160,22 +238,34 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
||||
}}
|
||||
/>
|
||||
</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'}>
|
||||
<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,
|
||||
backgroundColor: theme.palette.general.primary,
|
||||
color: theme.palette.text.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.general.disabled
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Option value="Select Auth" disabled>Select Auth</Option>
|
||||
@@ -192,9 +282,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={form.password}
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({ ...prev, password: e.target.value }))
|
||||
}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.primary,
|
||||
color: theme.palette.text.primary,
|
||||
@@ -215,53 +303,48 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
||||
)}
|
||||
|
||||
{form.authMethod === 'rsaKey' && form.storePassword && (
|
||||
<FormControl
|
||||
error={!form.rsaKey && !hostConfig?.rsaKey}
|
||||
sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
|
||||
>
|
||||
<FormControl error={!form.rsaKey && !hostConfig?.rsaKey}>
|
||||
<FormLabel>RSA Key</FormLabel>
|
||||
<Input
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
<Button
|
||||
component="label"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.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 && (
|
||||
<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.
|
||||
</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>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -271,12 +354,18 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
||||
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>
|
||||
</Stack>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</ModalDialog>
|
||||
|
||||
@@ -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"
|
||||
<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,
|
||||
color: theme.palette.text.primary,
|
||||
padding: 3,
|
||||
borderRadius: 10,
|
||||
width: "auto",
|
||||
maxWidth: "90vw",
|
||||
minWidth: "fit-content",
|
||||
borderWidth: "1px",
|
||||
borderStyle: "solid",
|
||||
borderRadius: "0.5rem",
|
||||
width: "400px",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
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 }}>
|
||||
}}>
|
||||
<div className="p-4 flex flex-col gap-4">
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
fullWidth
|
||||
onClick={handleLogoutUser}
|
||||
startDecorator={<LogoutIcon />}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.general.disabled,
|
||||
backgroundColor: theme.palette.general.tertiary,
|
||||
color: "white",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.general.secondary,
|
||||
},
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
Delete User
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleLogout}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.general.disabled,
|
||||
},
|
||||
width: "100%",
|
||||
height: "40px",
|
||||
border: `1px solid ${theme.palette.general.secondary}`,
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</ModalDialog>
|
||||
|
||||
<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>
|
||||
</CssVarsProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
ProfileModal.propTypes = {
|
||||
isHidden: PropTypes.bool.isRequired,
|
||||
@@ -104,5 +86,3 @@ ProfileModal.propTypes = {
|
||||
handleLogoutUser: PropTypes.func.isRequired,
|
||||
setIsProfileHidden: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProfileModal;
|
||||
Reference in New Issue
Block a user