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 [nextId, setNextId] = useState(1);
|
||||||
const [addHostForm, setAddHostForm] = useState({
|
const [addHostForm, setAddHostForm] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
|
folder: "",
|
||||||
ip: "",
|
ip: "",
|
||||||
user: "",
|
user: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -41,6 +42,7 @@ function App() {
|
|||||||
});
|
});
|
||||||
const [editHostForm, setEditHostForm] = useState({
|
const [editHostForm, setEditHostForm] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
|
folder: "",
|
||||||
ip: "",
|
ip: "",
|
||||||
user: "",
|
user: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -66,6 +68,7 @@ function App() {
|
|||||||
const [splitTabIds, setSplitTabIds] = useState([]);
|
const [splitTabIds, setSplitTabIds] = useState([]);
|
||||||
const [isEditHostHidden, setIsEditHostHidden] = useState(true);
|
const [isEditHostHidden, setIsEditHostHidden] = useState(true);
|
||||||
const [currentHostConfig, setCurrentHostConfig] = useState(null);
|
const [currentHostConfig, setCurrentHostConfig] = useState(null);
|
||||||
|
const [isLoggingIn, setIsLoggingIn] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
@@ -124,23 +127,97 @@ function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sessionToken = localStorage.getItem('sessionToken');
|
const sessionToken = localStorage.getItem('sessionToken');
|
||||||
if (sessionToken) {
|
let isComponentMounted = true;
|
||||||
setTimeout(() => {
|
let isLoginInProgress = false;
|
||||||
handleLoginUser({
|
|
||||||
|
if (userRef.current?.getUser()) {
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
setIsLoginUserHidden(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionToken) {
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
setIsLoginUserHidden(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoggingIn(true);
|
||||||
|
let loginAttempts = 0;
|
||||||
|
const maxAttempts = 50;
|
||||||
|
let attemptLoginInterval;
|
||||||
|
|
||||||
|
const loginTimeout = setTimeout(() => {
|
||||||
|
if (isComponentMounted) {
|
||||||
|
clearInterval(attemptLoginInterval);
|
||||||
|
if (!userRef.current?.getUser()) {
|
||||||
|
localStorage.removeItem('sessionToken');
|
||||||
|
setIsLoginUserHidden(false);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
setErrorMessage('Login timed out. Please try again.');
|
||||||
|
setIsErrorHidden(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
const attemptLogin = () => {
|
||||||
|
if (!isComponentMounted || isLoginInProgress) return;
|
||||||
|
|
||||||
|
if (loginAttempts >= maxAttempts || userRef.current?.getUser()) {
|
||||||
|
clearTimeout(loginTimeout);
|
||||||
|
clearInterval(attemptLoginInterval);
|
||||||
|
|
||||||
|
if (!userRef.current?.getUser()) {
|
||||||
|
localStorage.removeItem('sessionToken');
|
||||||
|
setIsLoginUserHidden(false);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
setErrorMessage('Login timed out. Please try again.');
|
||||||
|
setIsErrorHidden(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userRef.current) {
|
||||||
|
isLoginInProgress = true;
|
||||||
|
userRef.current.loginUser({
|
||||||
sessionToken,
|
sessionToken,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
if (isComponentMounted) {
|
||||||
|
clearTimeout(loginTimeout);
|
||||||
|
clearInterval(attemptLoginInterval);
|
||||||
setIsLoginUserHidden(true);
|
setIsLoginUserHidden(true);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
setIsErrorHidden(true);
|
||||||
|
}
|
||||||
|
isLoginInProgress = false;
|
||||||
},
|
},
|
||||||
onFailure: (error) => {
|
onFailure: (error) => {
|
||||||
|
if (isComponentMounted) {
|
||||||
|
if (!userRef.current?.getUser()) {
|
||||||
|
clearTimeout(loginTimeout);
|
||||||
|
clearInterval(attemptLoginInterval);
|
||||||
|
localStorage.removeItem('sessionToken');
|
||||||
setErrorMessage(`Auto-login failed: ${error}`);
|
setErrorMessage(`Auto-login failed: ${error}`);
|
||||||
setIsErrorHidden(false);
|
setIsErrorHidden(false);
|
||||||
setIsLoginUserHidden(false);
|
setIsLoginUserHidden(false);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLoginInProgress = false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, 500);
|
|
||||||
} else {
|
|
||||||
setIsLoginUserHidden(false);
|
|
||||||
}
|
}
|
||||||
|
loginAttempts++;
|
||||||
|
};
|
||||||
|
|
||||||
|
attemptLoginInterval = setInterval(attemptLogin, 100);
|
||||||
|
attemptLogin();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isComponentMounted = false;
|
||||||
|
clearTimeout(loginTimeout);
|
||||||
|
clearInterval(attemptLoginInterval);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAddHost = () => {
|
const handleAddHost = () => {
|
||||||
@@ -168,6 +245,8 @@ function App() {
|
|||||||
id: nextId,
|
id: nextId,
|
||||||
title: addHostForm.name || addHostForm.ip,
|
title: addHostForm.name || addHostForm.ip,
|
||||||
hostConfig: {
|
hostConfig: {
|
||||||
|
name: addHostForm.name,
|
||||||
|
folder: addHostForm.folder,
|
||||||
ip: addHostForm.ip,
|
ip: addHostForm.ip,
|
||||||
user: addHostForm.user,
|
user: addHostForm.user,
|
||||||
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
||||||
@@ -180,7 +259,7 @@ function App() {
|
|||||||
setActiveTab(nextId);
|
setActiveTab(nextId);
|
||||||
setNextId(nextId + 1);
|
setNextId(nextId + 1);
|
||||||
setIsAddHostHidden(true);
|
setIsAddHostHidden(true);
|
||||||
setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" });
|
setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth", rememberHost: false, storePassword: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAuthSubmit = (form) => {
|
const handleAuthSubmit = (form) => {
|
||||||
@@ -217,6 +296,7 @@ function App() {
|
|||||||
const handleSaveHost = () => {
|
const handleSaveHost = () => {
|
||||||
let hostConfig = {
|
let hostConfig = {
|
||||||
name: addHostForm.name || addHostForm.ip,
|
name: addHostForm.name || addHostForm.ip,
|
||||||
|
folder: addHostForm.folder,
|
||||||
ip: addHostForm.ip,
|
ip: addHostForm.ip,
|
||||||
user: addHostForm.user,
|
user: addHostForm.user,
|
||||||
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
||||||
@@ -235,15 +315,32 @@ function App() {
|
|||||||
if (sessionToken) {
|
if (sessionToken) {
|
||||||
userRef.current.loginUser({
|
userRef.current.loginUser({
|
||||||
sessionToken,
|
sessionToken,
|
||||||
onSuccess,
|
onSuccess: () => {
|
||||||
onFailure,
|
setIsLoginUserHidden(true);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
},
|
||||||
|
onFailure: (error) => {
|
||||||
|
localStorage.removeItem('sessionToken');
|
||||||
|
setIsLoginUserHidden(false);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
if (onFailure) onFailure(error);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
userRef.current.loginUser({
|
userRef.current.loginUser({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
onSuccess,
|
onSuccess: () => {
|
||||||
onFailure,
|
setIsLoginUserHidden(true);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
},
|
||||||
|
onFailure: (error) => {
|
||||||
|
setIsLoginUserHidden(false);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
if (onFailure) onFailure(error);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,7 +361,7 @@ function App() {
|
|||||||
onFailure,
|
onFailure,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDeleteUser = ({ onSuccess, onFailure }) => {
|
const handleDeleteUser = ({ onSuccess, onFailure }) => {
|
||||||
if (userRef.current) {
|
if (userRef.current) {
|
||||||
@@ -311,31 +408,21 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditHost = async () => {
|
const handleEditHost = async (oldConfig, newConfig = null) => {
|
||||||
try {
|
try {
|
||||||
// Only clear the password if switching to RSA or storePassword is false
|
if (newConfig) {
|
||||||
if (editHostForm.authMethod === 'rsaKey') {
|
|
||||||
editHostForm.password = '';
|
|
||||||
} else if (!editHostForm.storePassword) {
|
|
||||||
editHostForm.password = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
await userRef.current.editHost({
|
await userRef.current.editHost({
|
||||||
oldHostConfig: currentHostConfig,
|
oldHostConfig: oldConfig,
|
||||||
newHostConfig: editHostForm,
|
newHostConfig: newConfig,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
// Refresh the updated config
|
|
||||||
const refreshedHosts = await userRef.current.getAllHosts();
|
|
||||||
const updated = refreshedHosts.find(
|
|
||||||
(h) => h.config.ip === editHostForm.ip && h.config.user === editHostForm.user
|
|
||||||
);
|
|
||||||
if (updated) {
|
|
||||||
setCurrentHostConfig(updated.config);
|
|
||||||
}
|
}
|
||||||
setIsEditHostHidden(true);
|
|
||||||
|
updateEditHostForm(oldConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Edit failed: ' + error);
|
console.error('Edit failed:', error);
|
||||||
|
setErrorMessage(`Edit failed: ${error}`);
|
||||||
|
setIsErrorHidden(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -398,8 +485,11 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-4">
|
||||||
{/* Launchpad Button */}
|
{/* Launchpad Button */}
|
||||||
<Button
|
<Button
|
||||||
|
disabled={isLoggingIn || !userRef.current?.getUser()}
|
||||||
onClick={() => setIsLaunchpadOpen(true)}
|
onClick={() => setIsLaunchpadOpen(true)}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.tertiary,
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
@@ -408,33 +498,21 @@ function App() {
|
|||||||
height: "52px",
|
height: "52px",
|
||||||
width: "52px",
|
width: "52px",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
opacity: (!userRef.current?.getUser() || isLoggingIn) ? 0.3 : 1,
|
||||||
|
cursor: (!userRef.current?.getUser() || isLoggingIn) ? 'not-allowed' : 'pointer',
|
||||||
|
"&:disabled": {
|
||||||
|
opacity: 0.3,
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70", objectFit: "contain" }} />
|
<img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70%", objectFit: "contain" }} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Add Host Button */}
|
{/* Add Host Button */}
|
||||||
<Button
|
<Button
|
||||||
|
disabled={isLoggingIn || !userRef.current?.getUser()}
|
||||||
onClick={() => setIsAddHostHidden(false)}
|
onClick={() => setIsAddHostHidden(false)}
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.tertiary,
|
|
||||||
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
|
||||||
flexShrink: 0,
|
|
||||||
height: "52px",
|
|
||||||
width: "52px",
|
|
||||||
fontSize: "3.5rem",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
paddingTop: "2px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Profile Button */}
|
|
||||||
<Button
|
|
||||||
onClick={() => setIsProfileHidden(false)}
|
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.tertiary,
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
||||||
@@ -445,6 +523,41 @@ function App() {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
opacity: (!userRef.current?.getUser() || isLoggingIn) ? 0.3 : 1,
|
||||||
|
cursor: (!userRef.current?.getUser() || isLoggingIn) ? 'not-allowed' : 'pointer',
|
||||||
|
"&:disabled": {
|
||||||
|
opacity: 0.3,
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
},
|
||||||
|
fontSize: "4rem",
|
||||||
|
fontWeight: "600",
|
||||||
|
lineHeight: "0",
|
||||||
|
paddingBottom: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Profile Button */}
|
||||||
|
<Button
|
||||||
|
disabled={isLoggingIn}
|
||||||
|
onClick={() => userRef.current?.getUser() ? setIsProfileHidden(false) : setIsLoginUserHidden(false)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
||||||
|
flexShrink: 0,
|
||||||
|
height: "52px",
|
||||||
|
width: "52px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 0,
|
||||||
|
opacity: isLoggingIn ? 0.3 : 1,
|
||||||
|
cursor: isLoggingIn ? 'not-allowed' : 'pointer',
|
||||||
|
"&:disabled": {
|
||||||
|
opacity: 0.3,
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -454,10 +567,12 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Terminal Views */}
|
{/* Terminal Views */}
|
||||||
<div className={`relative p-4 terminal-container ${getLayoutStyle()}`}>
|
<div className={`relative p-4 terminal-container ${getLayoutStyle()}`}>
|
||||||
{terminals.map((terminal) => (
|
{userRef.current?.getUser() ? (
|
||||||
|
terminals.map((terminal) => (
|
||||||
<div
|
<div
|
||||||
key={terminal.id}
|
key={terminal.id}
|
||||||
className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${
|
className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${
|
||||||
@@ -479,7 +594,15 @@ function App() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center text-neutral-400">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Welcome to Termix</h2>
|
||||||
|
<p>{isLoggingIn ? "Checking login status..." : "Please login to start managing your SSH connections"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<NoAuthenticationModal
|
<NoAuthenticationModal
|
||||||
isHidden={isNoAuthHidden}
|
isHidden={isNoAuthHidden}
|
||||||
form={authForm}
|
form={authForm}
|
||||||
@@ -488,9 +611,10 @@ function App() {
|
|||||||
handleAuthSubmit={handleAuthSubmit}
|
handleAuthSubmit={handleAuthSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
|
{userRef.current?.getUser() && (
|
||||||
|
<>
|
||||||
<AddHostModal
|
<AddHostModal
|
||||||
isHidden={isAddHostHidden}
|
isHidden={isAddHostHidden}
|
||||||
form={addHostForm}
|
form={addHostForm}
|
||||||
@@ -506,14 +630,6 @@ function App() {
|
|||||||
setIsEditHostHidden={setIsEditHostHidden}
|
setIsEditHostHidden={setIsEditHostHidden}
|
||||||
hostConfig={currentHostConfig}
|
hostConfig={currentHostConfig}
|
||||||
/>
|
/>
|
||||||
<CreateUserModal
|
|
||||||
isHidden={isCreateUserHidden}
|
|
||||||
form={createUserForm}
|
|
||||||
setForm={setCreateUserForm}
|
|
||||||
handleCreateUser={handleCreateUser}
|
|
||||||
setIsCreateUserHidden={setIsCreateUserHidden}
|
|
||||||
setIsLoginUserHidden={setIsLoginUserHidden}
|
|
||||||
/>
|
|
||||||
<ProfileModal
|
<ProfileModal
|
||||||
isHidden={isProfileHidden}
|
isHidden={isProfileHidden}
|
||||||
getUser={getUser}
|
getUser={getUser}
|
||||||
@@ -521,11 +637,6 @@ function App() {
|
|||||||
handleLogoutUser={handleLogoutUser}
|
handleLogoutUser={handleLogoutUser}
|
||||||
setIsProfileHidden={setIsProfileHidden}
|
setIsProfileHidden={setIsProfileHidden}
|
||||||
/>
|
/>
|
||||||
<ErrorModal
|
|
||||||
isHidden={isErrorHidden}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
setIsErrorHidden={setIsErrorHidden}
|
|
||||||
/>
|
|
||||||
{isLaunchpadOpen && (
|
{isLaunchpadOpen && (
|
||||||
<Launchpad
|
<Launchpad
|
||||||
onClose={() => setIsLaunchpadOpen(false)}
|
onClose={() => setIsLaunchpadOpen(false)}
|
||||||
@@ -536,9 +647,17 @@ function App() {
|
|||||||
isEditHostHidden={isEditHostHidden}
|
isEditHostHidden={isEditHostHidden}
|
||||||
isErrorHidden={isErrorHidden}
|
isErrorHidden={isErrorHidden}
|
||||||
deleteHost={deleteHost}
|
deleteHost={deleteHost}
|
||||||
editHost={updateEditHostForm}
|
editHost={handleEditHost}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ErrorModal
|
||||||
|
isHidden={isErrorHidden}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
setIsErrorHidden={setIsErrorHidden}
|
||||||
|
/>
|
||||||
|
|
||||||
<LoginUserModal
|
<LoginUserModal
|
||||||
isHidden={isLoginUserHidden}
|
isHidden={isLoginUserHidden}
|
||||||
@@ -550,14 +669,39 @@ function App() {
|
|||||||
setIsCreateUserHidden={setIsCreateUserHidden}
|
setIsCreateUserHidden={setIsCreateUserHidden}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CreateUserModal
|
||||||
|
isHidden={isCreateUserHidden}
|
||||||
|
form={createUserForm}
|
||||||
|
setForm={setCreateUserForm}
|
||||||
|
handleCreateUser={handleCreateUser}
|
||||||
|
setIsCreateUserHidden={setIsCreateUserHidden}
|
||||||
|
setIsLoginUserHidden={setIsLoginUserHidden}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* User component */}
|
{/* User component */}
|
||||||
<User
|
<User
|
||||||
ref={userRef}
|
ref={userRef}
|
||||||
onLoginSuccess={() => setIsLoginUserHidden(true)}
|
onLoginSuccess={() => {
|
||||||
|
setIsLoginUserHidden(true);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
setIsErrorHidden(true);
|
||||||
|
}}
|
||||||
onCreateSuccess={() => {
|
onCreateSuccess={() => {
|
||||||
setIsCreateUserHidden(true);
|
setIsCreateUserHidden(true);
|
||||||
handleLoginUser({ username: createUserForm.username, password: createUserForm.password })}
|
handleLoginUser({
|
||||||
|
username: createUserForm.username,
|
||||||
|
password: createUserForm.password,
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsLoginUserHidden(true);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
setIsErrorHidden(true);
|
||||||
|
},
|
||||||
|
onFailure: (error) => {
|
||||||
|
setErrorMessage(`Login failed: ${error}`);
|
||||||
|
setIsErrorHidden(false);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
onDeleteSuccess={() => {
|
onDeleteSuccess={() => {
|
||||||
setIsProfileHidden(true);
|
setIsProfileHidden(true);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -565,9 +709,11 @@ function App() {
|
|||||||
onFailure={(error) => {
|
onFailure={(error) => {
|
||||||
setErrorMessage(`Action failed: ${error}`);
|
setErrorMessage(`Action failed: ${error}`);
|
||||||
setIsErrorHidden(false);
|
setIsErrorHidden(false);
|
||||||
|
setIsLoggingIn(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</CssVarsProvider>
|
</CssVarsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import { CssVarsProvider } from '@mui/joy/styles';
|
|||||||
import { Button } from '@mui/joy';
|
import { Button } from '@mui/joy';
|
||||||
import HostViewerIcon from '../images/host_viewer_icon.png';
|
import HostViewerIcon from '../images/host_viewer_icon.png';
|
||||||
import theme from '../theme.js';
|
import theme from '../theme.js';
|
||||||
|
|
||||||
// Apps
|
|
||||||
import HostViewer from './ssh/HostViewer.jsx';
|
import HostViewer from './ssh/HostViewer.jsx';
|
||||||
|
|
||||||
function Launchpad({onClose,
|
function Launchpad({
|
||||||
|
onClose,
|
||||||
getHosts,
|
getHosts,
|
||||||
connectToHost,
|
connectToHost,
|
||||||
isAddHostHidden,
|
isAddHostHidden,
|
||||||
@@ -17,7 +16,7 @@ function Launchpad({onClose,
|
|||||||
isErrorHidden,
|
isErrorHidden,
|
||||||
deleteHost,
|
deleteHost,
|
||||||
editHost,
|
editHost,
|
||||||
}) {
|
}) {
|
||||||
const launchpadRef = useRef(null);
|
const launchpadRef = useRef(null);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const [activeApp, setActiveApp] = useState('hostViewer');
|
const [activeApp, setActiveApp] = useState('hostViewer');
|
||||||
@@ -42,11 +41,6 @@ function Launchpad({onClose,
|
|||||||
};
|
};
|
||||||
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]);
|
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]);
|
||||||
|
|
||||||
const handleEditHostClick = () => {
|
|
||||||
setIsAddHostHidden(false);
|
|
||||||
setActiveApp('hostViewer');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CssVarsProvider theme={theme}>
|
<CssVarsProvider theme={theme}>
|
||||||
<div
|
<div
|
||||||
@@ -163,7 +157,7 @@ function Launchpad({onClose,
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
<div style={{ flex: 1, overflow: 'hidden' }}>
|
||||||
{activeApp === 'hostViewer' && (
|
{activeApp === 'hostViewer' && (
|
||||||
<HostViewer
|
<HostViewer
|
||||||
getHosts={getHosts}
|
getHosts={getHosts}
|
||||||
@@ -171,7 +165,7 @@ function Launchpad({onClose,
|
|||||||
setIsAddHostHidden={setIsAddHostHidden}
|
setIsAddHostHidden={setIsAddHostHidden}
|
||||||
deleteHost={deleteHost}
|
deleteHost={deleteHost}
|
||||||
editHost={editHost}
|
editHost={editHost}
|
||||||
onEditHostClick={handleEditHostClick}
|
openEditPanel={editHost}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import PropTypes from "prop-types";
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Button, Input } from "@mui/joy";
|
import { Button, Input } from "@mui/joy";
|
||||||
|
|
||||||
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost }) {
|
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost, openEditPanel }) {
|
||||||
const [hosts, setHosts] = useState([]);
|
const [hosts, setHosts] = useState([]);
|
||||||
const [filteredHosts, setFilteredHosts] = useState([]);
|
const [filteredHosts, setFilteredHosts] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [collapsedFolders, setCollapsedFolders] = useState(new Set());
|
||||||
|
const [draggedHost, setDraggedHost] = useState(null);
|
||||||
|
const [isDraggingOver, setIsDraggingOver] = useState(null);
|
||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
|
|
||||||
const fetchHosts = async () => {
|
const fetchHosts = async () => {
|
||||||
@@ -44,11 +47,181 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filtered = hosts.filter((hostWrapper) => {
|
const filtered = hosts.filter((hostWrapper) => {
|
||||||
const hostConfig = hostWrapper.config || {};
|
const hostConfig = hostWrapper.config || {};
|
||||||
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) || hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase());
|
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
hostConfig.folder?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
});
|
});
|
||||||
setFilteredHosts(filtered);
|
setFilteredHosts(filtered);
|
||||||
}, [searchTerm, hosts]);
|
}, [searchTerm, hosts]);
|
||||||
|
|
||||||
|
const toggleFolder = (folderName) => {
|
||||||
|
setCollapsedFolders(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(folderName)) {
|
||||||
|
newSet.delete(folderName);
|
||||||
|
} else {
|
||||||
|
newSet.add(folderName);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupHostsByFolder = (hosts) => {
|
||||||
|
const grouped = {};
|
||||||
|
const noFolder = [];
|
||||||
|
|
||||||
|
const sortedHosts = [...hosts].sort((a, b) => {
|
||||||
|
const nameA = (a.config?.name || a.config?.ip || '').toLowerCase();
|
||||||
|
const nameB = (b.config?.name || b.config?.ip || '').toLowerCase();
|
||||||
|
return nameA.localeCompare(nameB);
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedHosts.forEach(host => {
|
||||||
|
const folder = host.config?.folder;
|
||||||
|
if (folder) {
|
||||||
|
if (!grouped[folder]) {
|
||||||
|
grouped[folder] = [];
|
||||||
|
}
|
||||||
|
grouped[folder].push(host);
|
||||||
|
} else {
|
||||||
|
noFolder.push(host);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedFolders = Object.keys(grouped).sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
return { grouped, sortedFolders, noFolder };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragStart = (e, host) => {
|
||||||
|
setDraggedHost(host);
|
||||||
|
e.dataTransfer.setData('text/plain', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e, folderName) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDraggingOver(folderName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = () => {
|
||||||
|
setIsDraggingOver(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = async (e, targetFolder) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDraggingOver(null);
|
||||||
|
|
||||||
|
if (!draggedHost) return;
|
||||||
|
|
||||||
|
if (draggedHost.config.folder === targetFolder) return;
|
||||||
|
|
||||||
|
const newConfig = {
|
||||||
|
...draggedHost.config,
|
||||||
|
folder: targetFolder
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await editHost(draggedHost.config, newConfig);
|
||||||
|
await fetchHosts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update folder:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraggedHost(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDropOnNoFolder = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDraggingOver(null);
|
||||||
|
|
||||||
|
if (!draggedHost || !draggedHost.config.folder) return;
|
||||||
|
|
||||||
|
const newConfig = {
|
||||||
|
...draggedHost.config,
|
||||||
|
folder: null
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await editHost(draggedHost.config, newConfig);
|
||||||
|
await fetchHosts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove from folder:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraggedHost(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHostItem = (hostWrapper) => {
|
||||||
|
const hostConfig = hostWrapper.config || {};
|
||||||
|
|
||||||
|
if (!hostConfig) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={hostWrapper._id}
|
||||||
|
className={`flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 w-full cursor-grab active:cursor-grabbing hover:border-neutral-500 transition-colors ${draggedHost === hostWrapper ? 'opacity-50' : ''}`}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, hostWrapper)}
|
||||||
|
onDragEnd={() => setDraggedHost(null)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 flex-1">
|
||||||
|
<div className="text-neutral-500 cursor-grab active:cursor-grabbing">⋮⋮</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p>
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
className="text-black"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
connectToHost(hostConfig);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#6e6e6e",
|
||||||
|
"&:hover": { backgroundColor: "#0f0f0f" }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="text-black"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteHost({ ...hostConfig, _id: hostWrapper._id });
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#6e6e6e",
|
||||||
|
"&:hover": { backgroundColor: "#0f0f0f" }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="text-black"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openEditPanel(hostConfig);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#6e6e6e",
|
||||||
|
"&:hover": { backgroundColor: "#0f0f0f" }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 text-white flex flex-col">
|
<div className="h-full w-full p-4 text-white flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-2 w-full gap-2">
|
<div className="flex items-center justify-between mb-2 w-full gap-2">
|
||||||
@@ -79,60 +252,51 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
<p className="text-gray-300">Loading hosts...</p>
|
<p className="text-gray-300">Loading hosts...</p>
|
||||||
) : filteredHosts.length > 0 ? (
|
) : filteredHosts.length > 0 ? (
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
{filteredHosts.map((hostWrapper, index) => {
|
{(() => {
|
||||||
const hostConfig = hostWrapper.config || {};
|
const { grouped, sortedFolders, noFolder } = groupHostsByFolder(filteredHosts);
|
||||||
|
|
||||||
if (!hostConfig) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 w-full">
|
<>
|
||||||
<div>
|
{/* Render hosts without folders first */}
|
||||||
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p>
|
<div
|
||||||
<p className="text-sm text-gray-400">
|
className={`flex flex-col gap-2 p-2 rounded-lg transition-colors ${isDraggingOver === 'no-folder' ? 'bg-neutral-700' : ''}`}
|
||||||
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`}
|
onDragOver={(e) => handleDragOver(e, 'no-folder')}
|
||||||
</p>
|
onDragLeave={handleDragLeave}
|
||||||
</div>
|
onDrop={handleDropOnNoFolder}
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
className="text-black"
|
|
||||||
onClick={() => connectToHost(hostConfig)}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#6e6e6e",
|
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Connect
|
{noFolder.map((host) => renderHostItem(host))}
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="text-black"
|
|
||||||
onClick={() => {
|
|
||||||
deleteHost({ ...hostConfig, _id: hostWrapper._id });
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#6e6e6e",
|
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="text-black"
|
|
||||||
onClick={() => {
|
|
||||||
editHost(hostConfig);
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#6e6e6e",
|
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Render folders and their hosts */}
|
||||||
|
{sortedFolders.map((folderName) => (
|
||||||
|
<div key={folderName} className="mb-2">
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-2 p-2 bg-neutral-600 rounded-lg cursor-pointer hover:bg-neutral-500 transition-colors ${
|
||||||
|
isDraggingOver === folderName ? 'bg-neutral-500 border-2 border-dashed border-neutral-400' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => toggleFolder(folderName)}
|
||||||
|
onDragOver={(e) => handleDragOver(e, folderName)}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={(e) => handleDrop(e, folderName)}
|
||||||
|
>
|
||||||
|
<span className={`font-bold w-4 text-center transition-transform ${collapsedFolders.has(folderName) ? 'rotate-[-90deg]' : ''}`}>
|
||||||
|
▼
|
||||||
|
</span>
|
||||||
|
<span className="font-bold">{folderName}</span>
|
||||||
|
<span className="text-sm text-gray-300">
|
||||||
|
({grouped[folderName].length})
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{!collapsedFolders.has(folderName) && (
|
||||||
|
<div className="ml-6 mt-2 flex flex-col gap-2">
|
||||||
|
{grouped[folderName].map((host) => renderHostItem(host))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-300">No hosts available...</p>
|
<p className="text-gray-300">No hosts available...</p>
|
||||||
@@ -148,6 +312,7 @@ HostViewer.propTypes = {
|
|||||||
setIsAddHostHidden: PropTypes.func.isRequired,
|
setIsAddHostHidden: PropTypes.func.isRequired,
|
||||||
deleteHost: PropTypes.func.isRequired,
|
deleteHost: PropTypes.func.isRequired,
|
||||||
editHost: PropTypes.func.isRequired,
|
editHost: PropTypes.func.isRequired,
|
||||||
|
openEditPanel: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HostViewer;
|
export default HostViewer;
|
||||||
@@ -28,7 +28,8 @@ const hostSchema = new mongoose.Schema({
|
|||||||
name: { type: String, required: true },
|
name: { type: String, required: true },
|
||||||
config: { type: String, required: true },
|
config: { type: String, required: true },
|
||||||
users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
|
users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
|
||||||
createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
|
createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
|
||||||
|
folder: { type: String, default: null }
|
||||||
});
|
});
|
||||||
|
|
||||||
const User = mongoose.model('User', userSchema);
|
const User = mongoose.model('User', userSchema);
|
||||||
@@ -178,7 +179,8 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleanConfig = {
|
const cleanConfig = {
|
||||||
name: hostConfig.name.trim(),
|
name: hostConfig.name?.trim(),
|
||||||
|
folder: hostConfig.folder?.trim() || null,
|
||||||
ip: hostConfig.ip.trim(),
|
ip: hostConfig.ip.trim(),
|
||||||
user: hostConfig.user.trim(),
|
user: hostConfig.user.trim(),
|
||||||
port: hostConfig.port || 22,
|
port: hostConfig.port || 22,
|
||||||
@@ -208,7 +210,8 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
name: finalName,
|
name: finalName,
|
||||||
config: encryptedConfig,
|
config: encryptedConfig,
|
||||||
users: [userId],
|
users: [userId],
|
||||||
createdBy: userId
|
createdBy: userId,
|
||||||
|
folder: cleanConfig.folder
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Host created successfully: ${finalName}`);
|
logger.info(`Host created successfully: ${finalName}`);
|
||||||
@@ -360,10 +363,11 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleanConfig = {
|
const cleanConfig = {
|
||||||
|
name: newHostConfig.name?.trim(),
|
||||||
|
folder: newHostConfig.folder?.trim() || null,
|
||||||
ip: newHostConfig.ip.trim(),
|
ip: newHostConfig.ip.trim(),
|
||||||
user: newHostConfig.user.trim(),
|
user: newHostConfig.user.trim(),
|
||||||
port: newHostConfig.port || 22,
|
port: newHostConfig.port || 22,
|
||||||
name: newHostConfig.name.trim(),
|
|
||||||
password: newHostConfig.password?.trim() || undefined,
|
password: newHostConfig.password?.trim() || undefined,
|
||||||
rsaKey: newHostConfig.rsaKey?.trim() || undefined
|
rsaKey: newHostConfig.rsaKey?.trim() || undefined
|
||||||
};
|
};
|
||||||
@@ -375,6 +379,7 @@ io.of('/database.io').on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
host.config = encryptedConfig;
|
host.config = encryptedConfig;
|
||||||
|
host.folder = cleanConfig.folder;
|
||||||
await host.save();
|
await host.save();
|
||||||
|
|
||||||
logger.info(`Host edited successfully`);
|
logger.info(`Host edited successfully`);
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Option,
|
Option,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
IconButton
|
IconButton,
|
||||||
|
Tabs,
|
||||||
|
TabList,
|
||||||
|
Tab,
|
||||||
|
TabPanel
|
||||||
} from '@mui/joy';
|
} from '@mui/joy';
|
||||||
import theme from '/src/theme';
|
import theme from '/src/theme';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -22,6 +26,7 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|||||||
|
|
||||||
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
|
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
const handleFileChange = (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
@@ -68,7 +73,6 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
<CssVarsProvider theme={theme}>
|
<CssVarsProvider theme={theme}>
|
||||||
<Modal open={!isHidden} onClose={() => setIsAddHostHidden(true)}
|
<Modal open={!isHidden} onClose={() => setIsAddHostHidden(true)}
|
||||||
sx={{
|
sx={{
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -82,17 +86,57 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
padding: 3,
|
padding: 3,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
maxWidth: '400px',
|
maxWidth: '500px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflow: 'hidden',
|
maxHeight: '80vh',
|
||||||
|
overflow: 'auto',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
mx: 2,
|
mx: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle>Add Host</DialogTitle>
|
<DialogTitle sx={{ mb: 2 }}>Add Host</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Stack spacing={2} sx={{ width: '100%' }}>
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(e, val) => setActiveTab(val)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '8px',
|
||||||
|
marginBottom: '16px',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabList
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
gap: 0,
|
||||||
|
mb: 2,
|
||||||
|
'& button': {
|
||||||
|
flex: 1,
|
||||||
|
bgcolor: 'transparent',
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
'&.Mui-selected': {
|
||||||
|
bgcolor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: theme.palette.general.primary,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab>Basic Info</Tab>
|
||||||
|
<Tab>Connection</Tab>
|
||||||
|
<Tab>Authentication</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanel value={0}>
|
||||||
|
<Stack spacing={2}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>Host Name</FormLabel>
|
<FormLabel>Host Name</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
@@ -104,6 +148,22 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Folder</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.folder || ''}
|
||||||
|
onChange={(e) => setForm({ ...form, folder: e.target.value })}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={1}>
|
||||||
|
<Stack spacing={2}>
|
||||||
<FormControl error={!form.ip}>
|
<FormControl error={!form.ip}>
|
||||||
<FormLabel>Host IP</FormLabel>
|
<FormLabel>Host IP</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
@@ -128,6 +188,54 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormControl error={form.port < 1 || form.port > 65535}>
|
||||||
|
<FormLabel>Host Port</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={form.port}
|
||||||
|
onChange={(e) => setForm({ ...form, port: e.target.value })}
|
||||||
|
min={1}
|
||||||
|
max={65535}
|
||||||
|
required
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={2}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Remember Host</FormLabel>
|
||||||
|
<Checkbox
|
||||||
|
checked={form.rememberHost}
|
||||||
|
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&.Mui-checked': {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.rememberHost && (
|
||||||
|
<>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Store Password</FormLabel>
|
||||||
|
<Checkbox
|
||||||
|
checked={form.storePassword}
|
||||||
|
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&.Mui-checked': {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
|
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
|
||||||
<FormLabel>Authentication Method</FormLabel>
|
<FormLabel>Authentication Method</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -179,75 +287,57 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
{form.authMethod === 'rsaKey' && (
|
{form.authMethod === 'rsaKey' && (
|
||||||
<FormControl error={!form.rsaKey}>
|
<FormControl error={!form.rsaKey}>
|
||||||
<FormLabel>RSA Key</FormLabel>
|
<FormLabel>RSA Key</FormLabel>
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
required
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
padding: 1,
|
|
||||||
textAlign: 'center',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
<FormControl error={form.port < 1 || form.port > 65535}>
|
|
||||||
<FormLabel>Host Port</FormLabel>
|
|
||||||
<Input
|
|
||||||
value={form.port}
|
|
||||||
onChange={(e) => setForm({ ...form, port: e.target.value })}
|
|
||||||
min={1}
|
|
||||||
max={65535}
|
|
||||||
required
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>Remember Host</FormLabel>
|
|
||||||
<Checkbox
|
|
||||||
checked={form.rememberHost}
|
|
||||||
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
'&.Mui-checked': {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
{form.rememberHost && (
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>Store Password</FormLabel>
|
|
||||||
<Checkbox
|
|
||||||
checked={form.storePassword}
|
|
||||||
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
'&.Mui-checked': {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
component="label"
|
||||||
disabled={!isFormValid()}
|
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '40px',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.general.disabled,
|
backgroundColor: theme.palette.general.disabled,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
{form.rsaKey ? 'Change RSA Key File' : 'Upload RSA Key File'}
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
required
|
||||||
|
sx={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isFormValid()}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
'&:disabled': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
},
|
||||||
|
marginTop: 3,
|
||||||
|
width: '100%',
|
||||||
|
height: '40px',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Add Host
|
Add Host
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
@@ -260,6 +350,7 @@ AddHostModal.propTypes = {
|
|||||||
isHidden: PropTypes.bool.isRequired,
|
isHidden: PropTypes.bool.isRequired,
|
||||||
form: PropTypes.shape({
|
form: PropTypes.shape({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
|
folder: PropTypes.string,
|
||||||
ip: PropTypes.string.isRequired,
|
ip: PropTypes.string.isRequired,
|
||||||
user: PropTypes.string.isRequired,
|
user: PropTypes.string.isRequired,
|
||||||
password: PropTypes.string,
|
password: PropTypes.string,
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Option,
|
Option,
|
||||||
IconButton,
|
IconButton,
|
||||||
Checkbox
|
Checkbox,
|
||||||
|
Tabs,
|
||||||
|
TabList,
|
||||||
|
Tab,
|
||||||
|
TabPanel
|
||||||
} from '@mui/joy';
|
} from '@mui/joy';
|
||||||
import theme from '/src/theme';
|
import theme from '/src/theme';
|
||||||
import Visibility from '@mui/icons-material/Visibility';
|
import Visibility from '@mui/icons-material/Visibility';
|
||||||
@@ -22,25 +26,23 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|||||||
|
|
||||||
const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => {
|
const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hostConfig) {
|
if (hostConfig && !isHidden) {
|
||||||
const storePassword = hostConfig.password || hostConfig.rsaKey;
|
|
||||||
|
|
||||||
setForm({
|
setForm({
|
||||||
...form,
|
name: hostConfig.name || "",
|
||||||
name: hostConfig.name || '',
|
folder: hostConfig.folder || "",
|
||||||
ip: hostConfig.ip || '',
|
ip: hostConfig.ip || "",
|
||||||
user: hostConfig.user || '',
|
user: hostConfig.user || "",
|
||||||
password: storePassword && hostConfig.password ? hostConfig.password : '',
|
password: hostConfig.password || "",
|
||||||
rsaKey: '',
|
port: hostConfig.port || 22,
|
||||||
port: Number(hostConfig.port) || 22,
|
authMethod: hostConfig.password ? "password" : hostConfig.rsaKey ? "rsaKey" : "Select Auth",
|
||||||
authMethod: hostConfig.rsaKey ? 'rsaKey' : (storePassword ? 'password' : 'Select Auth'),
|
rememberHost: true,
|
||||||
rememberHost: hostConfig.rememberHost || true,
|
storePassword: true,
|
||||||
storePassword: storePassword ?? false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [hostConfig, setForm]);
|
}, [hostConfig, isHidden]);
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
const handleFileChange = (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
@@ -65,7 +67,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
const handleStorePasswordChange = (checked) => {
|
const handleStorePasswordChange = (checked) => {
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
storePassword: checked,
|
storePassword: Boolean(checked),
|
||||||
authMethod: checked ? 'password' : 'Select Auth'
|
authMethod: checked ? 'password' : 'Select Auth'
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -76,31 +78,38 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
const portNum = Number(port);
|
const portNum = Number(port);
|
||||||
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
|
||||||
|
|
||||||
if (storePassword && authMethod === 'password' && !password.trim()) return false;
|
if (Boolean(storePassword) && authMethod === 'password' && !password?.trim()) return false;
|
||||||
if (storePassword && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false;
|
if (Boolean(storePassword) && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false;
|
||||||
if (storePassword && authMethod === 'Select Auth') return false;
|
if (Boolean(storePassword) && authMethod === 'Select Auth') return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSave = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (isFormValid()) {
|
try {
|
||||||
const { authMethod, password, rsaKey, storePassword, ...rest } = form;
|
const newConfig = {
|
||||||
handleEditHost({
|
...form,
|
||||||
...rest,
|
port: String(form.port),
|
||||||
authMethod,
|
};
|
||||||
password: authMethod === 'password' && storePassword ? password : '',
|
|
||||||
rsaKey: authMethod === 'rsaKey' ? rsaKey : ''
|
if (form.authMethod === 'rsaKey' || !form.storePassword) {
|
||||||
});
|
newConfig.password = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleEditHost(hostConfig, newConfig);
|
||||||
|
setIsEditHostHidden(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CssVarsProvider theme={theme}>
|
<CssVarsProvider theme={theme}>
|
||||||
<Modal open={!isHidden} onClose={() => setIsEditHostHidden(true)}
|
<Modal
|
||||||
|
open={!isHidden}
|
||||||
|
onClose={() => setIsEditHostHidden(true)}
|
||||||
sx={{
|
sx={{
|
||||||
overflowX: 'hidden',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -114,17 +123,57 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
padding: 3,
|
padding: 3,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
maxWidth: '400px',
|
maxWidth: '500px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflow: 'hidden',
|
maxHeight: '80vh',
|
||||||
|
overflow: 'auto',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
mx: 2,
|
mx: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle>Edit Host</DialogTitle>
|
<DialogTitle sx={{ mb: 2 }}>Edit Host</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSave}>
|
||||||
<Stack spacing={2} sx={{ width: '100%', overflow: 'hidden' }}>
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(e, val) => setActiveTab(val)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '8px',
|
||||||
|
marginBottom: '16px',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabList
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
gap: 0,
|
||||||
|
mb: 2,
|
||||||
|
'& button': {
|
||||||
|
flex: 1,
|
||||||
|
bgcolor: 'transparent',
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
'&.Mui-selected': {
|
||||||
|
bgcolor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: theme.palette.general.primary,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab>Basic Info</Tab>
|
||||||
|
<Tab>Connection</Tab>
|
||||||
|
<Tab>Authentication</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanel value={0}>
|
||||||
|
<Stack spacing={2}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>Host Name</FormLabel>
|
<FormLabel>Host Name</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
@@ -137,6 +186,22 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Folder</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.folder}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, folder: e.target.value }))}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={1}>
|
||||||
|
<Stack spacing={2}>
|
||||||
<FormControl error={!form.ip}>
|
<FormControl error={!form.ip}>
|
||||||
<FormLabel>Host IP</FormLabel>
|
<FormLabel>Host IP</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
@@ -149,6 +214,19 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl error={form.port < 1 || form.port > 65535}>
|
||||||
|
<FormLabel>Host Port</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={form.port}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormControl error={!form.user}>
|
<FormControl error={!form.user}>
|
||||||
<FormLabel>Host User</FormLabel>
|
<FormLabel>Host User</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
@@ -160,22 +238,34 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
{form.storePassword && form.authMethod !== 'Select Auth' && (
|
<TabPanel value={2}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Store Password</FormLabel>
|
||||||
|
<Checkbox
|
||||||
|
checked={form.storePassword}
|
||||||
|
onChange={(e) => handleStorePasswordChange(e.target.checked)}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&.Mui-checked': {
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{form.storePassword && (
|
||||||
<FormControl error={form.authMethod === 'Select Auth'}>
|
<FormControl error={form.authMethod === 'Select Auth'}>
|
||||||
<FormLabel>Authentication Method</FormLabel>
|
<FormLabel>Authentication Method</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
value={form.authMethod}
|
value={form.authMethod}
|
||||||
onChange={(e, val) => handleAuthChange(val)}
|
onChange={(e, val) => handleAuthChange(val)}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor:
|
backgroundColor: theme.palette.general.primary,
|
||||||
form.authMethod === 'Select Auth'
|
|
||||||
? theme.palette.general.tertiary
|
|
||||||
: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.general.disabled
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Option value="Select Auth" disabled>Select Auth</Option>
|
<Option value="Select Auth" disabled>Select Auth</Option>
|
||||||
@@ -192,9 +282,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
<Input
|
<Input
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(e) =>
|
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
|
||||||
setForm((prev) => ({ ...prev, password: e.target.value }))
|
|
||||||
}
|
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
@@ -215,53 +303,48 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{form.authMethod === 'rsaKey' && form.storePassword && (
|
{form.authMethod === 'rsaKey' && form.storePassword && (
|
||||||
<FormControl
|
<FormControl error={!form.rsaKey && !hostConfig?.rsaKey}>
|
||||||
error={!form.rsaKey && !hostConfig?.rsaKey}
|
|
||||||
sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
|
|
||||||
>
|
|
||||||
<FormLabel>RSA Key</FormLabel>
|
<FormLabel>RSA Key</FormLabel>
|
||||||
<Input
|
<Button
|
||||||
type="file"
|
component="label"
|
||||||
onChange={handleFileChange}
|
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
alignItems: 'center'
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '40px',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
{form.rsaKey ? 'Change RSA Key File' : 'Upload RSA Key File'}
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
sx={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
|
</Button>
|
||||||
{hostConfig?.rsaKey && !form.rsaKey && (
|
{hostConfig?.rsaKey && !form.rsaKey && (
|
||||||
<FormLabel sx={{ color: theme.palette.text.secondary }}>
|
<FormLabel
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
mt: 1,
|
||||||
|
display: 'block',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
Existing key detected. Upload to replace.
|
Existing key detected. Upload to replace.
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
)}
|
)}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
</Stack>
|
||||||
<FormControl error={form.port < 1 || form.port > 65535}>
|
</TabPanel>
|
||||||
<FormLabel>Host Port</FormLabel>
|
</Tabs>
|
||||||
<Input
|
|
||||||
value={form.port}
|
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>Store Password</FormLabel>
|
|
||||||
<Checkbox
|
|
||||||
checked={form.storePassword}
|
|
||||||
onChange={(e) => handleStorePasswordChange(e.target.checked)}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
'&.Mui-checked': {
|
|
||||||
color: theme.palette.text.primary
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -271,12 +354,18 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
|
|||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.general.disabled
|
backgroundColor: theme.palette.general.disabled
|
||||||
}
|
},
|
||||||
|
'&:disabled': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
},
|
||||||
|
marginTop: 3,
|
||||||
|
width: '100%',
|
||||||
|
height: '40px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
|
|||||||
@@ -1,101 +1,83 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { CssVarsProvider } from '@mui/joy/styles';
|
import { Modal, Typography, Button } from "@mui/joy";
|
||||||
import { Modal, Button, DialogTitle, DialogContent, ModalDialog, Stack } from '@mui/joy';
|
import LogoutIcon from "@mui/icons-material/Logout";
|
||||||
import theme from '/src/theme';
|
import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
|
||||||
|
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
|
||||||
const ProfileModal = ({ isHidden, getUser, handleDeleteUser, handleLogoutUser, setIsProfileHidden }) => {
|
import theme from "../theme";
|
||||||
const handleDelete = () => {
|
|
||||||
handleDeleteUser({
|
|
||||||
onSuccess: () => {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
handleLogoutUser({
|
|
||||||
onSuccess: () => {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserName = () => {
|
|
||||||
const user = getUser();
|
|
||||||
return user ? user.username : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export default function ProfileModal({
|
||||||
|
isHidden,
|
||||||
|
getUser,
|
||||||
|
handleDeleteUser,
|
||||||
|
handleLogoutUser,
|
||||||
|
setIsProfileHidden,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<CssVarsProvider theme={theme}>
|
<Modal
|
||||||
<Modal open={!isHidden} onClose={() => setIsProfileHidden(true)}>
|
open={!isHidden}
|
||||||
<ModalDialog
|
onClose={() => setIsProfileHidden(true)}
|
||||||
layout="center"
|
|
||||||
sx={{
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
backgroundColor: theme.palette.general.tertiary,
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
borderColor: theme.palette.general.secondary,
|
borderColor: theme.palette.general.secondary,
|
||||||
color: theme.palette.text.primary,
|
borderWidth: "1px",
|
||||||
padding: 3,
|
borderStyle: "solid",
|
||||||
borderRadius: 10,
|
borderRadius: "0.5rem",
|
||||||
width: "auto",
|
width: "400px",
|
||||||
maxWidth: "90vw",
|
|
||||||
minWidth: "fit-content",
|
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
display: "flex",
|
}}>
|
||||||
flexDirection: "column",
|
<div className="p-4 flex flex-col gap-4">
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogTitle
|
|
||||||
sx={{
|
|
||||||
marginBottom: 1.5,
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
padding: 1,
|
|
||||||
borderRadius: 10,
|
|
||||||
width: "100%",
|
|
||||||
textAlign: "center",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
User: {getUserName()}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent sx={{ width: "100%" }}>
|
|
||||||
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden", mt: 1.5 }}>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDelete}
|
fullWidth
|
||||||
|
onClick={handleLogoutUser}
|
||||||
|
startDecorator={<LogoutIcon />}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
'&:hover': {
|
color: "white",
|
||||||
backgroundColor: theme.palette.general.disabled,
|
"&:hover": {
|
||||||
|
backgroundColor: theme.palette.general.secondary,
|
||||||
},
|
},
|
||||||
width: "100%",
|
height: "40px",
|
||||||
}}
|
border: `1px solid ${theme.palette.general.secondary}`,
|
||||||
>
|
|
||||||
Delete User
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleLogout}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.general.disabled,
|
|
||||||
},
|
|
||||||
width: "100%",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
|
||||||
</DialogContent>
|
<Button
|
||||||
</ModalDialog>
|
fullWidth
|
||||||
|
color="danger"
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm("Are you sure you want to delete your account? This action cannot be undone.")) {
|
||||||
|
handleDeleteUser({
|
||||||
|
onSuccess: () => setIsProfileHidden(true),
|
||||||
|
onFailure: (error) => console.error(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
startDecorator={<DeleteForeverIcon />}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#c53030",
|
||||||
|
color: "white",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#9b2c2c",
|
||||||
|
},
|
||||||
|
height: "40px",
|
||||||
|
border: "1px solid #9b2c2c",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</CssVarsProvider>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
ProfileModal.propTypes = {
|
ProfileModal.propTypes = {
|
||||||
isHidden: PropTypes.bool.isRequired,
|
isHidden: PropTypes.bool.isRequired,
|
||||||
@@ -104,5 +86,3 @@ ProfileModal.propTypes = {
|
|||||||
handleLogoutUser: PropTypes.func.isRequired,
|
handleLogoutUser: PropTypes.func.isRequired,
|
||||||
setIsProfileHidden: PropTypes.func.isRequired,
|
setIsProfileHidden: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfileModal;
|
|
||||||
Reference in New Issue
Block a user