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:
518
src/App.jsx
518
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: () => {
|
||||||
setIsLoginUserHidden(true);
|
if (isComponentMounted) {
|
||||||
|
clearTimeout(loginTimeout);
|
||||||
|
clearInterval(attemptLoginInterval);
|
||||||
|
setIsLoginUserHidden(true);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
setIsErrorHidden(true);
|
||||||
|
}
|
||||||
|
isLoginInProgress = false;
|
||||||
},
|
},
|
||||||
onFailure: (error) => {
|
onFailure: (error) => {
|
||||||
setErrorMessage(`Auto-login failed: ${error}`);
|
if (isComponentMounted) {
|
||||||
setIsErrorHidden(false);
|
if (!userRef.current?.getUser()) {
|
||||||
setIsLoginUserHidden(false);
|
clearTimeout(loginTimeout);
|
||||||
|
clearInterval(attemptLoginInterval);
|
||||||
|
localStorage.removeItem('sessionToken');
|
||||||
|
setErrorMessage(`Auto-login failed: ${error}`);
|
||||||
|
setIsErrorHidden(false);
|
||||||
|
setIsLoginUserHidden(false);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLoginInProgress = false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, 500);
|
}
|
||||||
} else {
|
loginAttempts++;
|
||||||
setIsLoginUserHidden(false);
|
};
|
||||||
}
|
|
||||||
|
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') {
|
await userRef.current.editHost({
|
||||||
editHostForm.password = '';
|
oldHostConfig: oldConfig,
|
||||||
} else if (!editHostForm.storePassword) {
|
newHostConfig: newConfig,
|
||||||
editHostForm.password = '';
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await userRef.current.editHost({
|
updateEditHostForm(oldConfig);
|
||||||
oldHostConfig: currentHostConfig,
|
|
||||||
newHostConfig: editHostForm,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Refresh the updated config
|
|
||||||
const refreshedHosts = await userRef.current.getAllHosts();
|
|
||||||
const updated = refreshedHosts.find(
|
|
||||||
(h) => h.config.ip === editHostForm.ip && h.config.user === editHostForm.user
|
|
||||||
);
|
|
||||||
if (updated) {
|
|
||||||
setCurrentHostConfig(updated.config);
|
|
||||||
}
|
|
||||||
setIsEditHostHidden(true);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Edit failed: ' + error);
|
console.error('Edit failed:', error);
|
||||||
|
setErrorMessage(`Edit failed: ${error}`);
|
||||||
|
setIsErrorHidden(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -398,88 +485,124 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Launchpad Button */}
|
{/* Action Buttons */}
|
||||||
<Button
|
<div className="flex gap-4">
|
||||||
onClick={() => setIsLaunchpadOpen(true)}
|
{/* Launchpad Button */}
|
||||||
sx={{
|
<Button
|
||||||
backgroundColor: theme.palette.general.tertiary,
|
disabled={isLoggingIn || !userRef.current?.getUser()}
|
||||||
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
onClick={() => setIsLaunchpadOpen(true)}
|
||||||
flexShrink: 0,
|
sx={{
|
||||||
height: "52px",
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
width: "52px",
|
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
||||||
padding: 0,
|
flexShrink: 0,
|
||||||
}}
|
height: "52px",
|
||||||
>
|
width: "52px",
|
||||||
<img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70", objectFit: "contain" }} />
|
padding: 0,
|
||||||
</Button>
|
opacity: (!userRef.current?.getUser() || isLoggingIn) ? 0.3 : 1,
|
||||||
|
cursor: (!userRef.current?.getUser() || isLoggingIn) ? 'not-allowed' : 'pointer',
|
||||||
|
"&:disabled": {
|
||||||
|
opacity: 0.3,
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70%", objectFit: "contain" }} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Add Host Button */}
|
{/* Add Host Button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsAddHostHidden(false)}
|
disabled={isLoggingIn || !userRef.current?.getUser()}
|
||||||
sx={{
|
onClick={() => setIsAddHostHidden(false)}
|
||||||
backgroundColor: theme.palette.general.tertiary,
|
sx={{
|
||||||
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
flexShrink: 0,
|
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
||||||
height: "52px",
|
flexShrink: 0,
|
||||||
width: "52px",
|
height: "52px",
|
||||||
fontSize: "3.5rem",
|
width: "52px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingTop: "2px",
|
padding: 0,
|
||||||
}}
|
opacity: (!userRef.current?.getUser() || isLoggingIn) ? 0.3 : 1,
|
||||||
>
|
cursor: (!userRef.current?.getUser() || isLoggingIn) ? 'not-allowed' : 'pointer',
|
||||||
+
|
"&:disabled": {
|
||||||
</Button>
|
opacity: 0.3,
|
||||||
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
|
},
|
||||||
|
fontSize: "4rem",
|
||||||
|
fontWeight: "600",
|
||||||
|
lineHeight: "0",
|
||||||
|
paddingBottom: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Profile Button */}
|
{/* Profile Button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsProfileHidden(false)}
|
disabled={isLoggingIn}
|
||||||
sx={{
|
onClick={() => userRef.current?.getUser() ? setIsProfileHidden(false) : setIsLoginUserHidden(false)}
|
||||||
backgroundColor: theme.palette.general.tertiary,
|
sx={{
|
||||||
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
flexShrink: 0,
|
"&:hover": { backgroundColor: theme.palette.general.secondary },
|
||||||
height: "52px",
|
flexShrink: 0,
|
||||||
width: "52px",
|
height: "52px",
|
||||||
display: "flex",
|
width: "52px",
|
||||||
justifyContent: "center",
|
display: "flex",
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
padding: 0,
|
alignItems: "center",
|
||||||
}}
|
padding: 0,
|
||||||
>
|
opacity: isLoggingIn ? 0.3 : 1,
|
||||||
<img
|
cursor: isLoggingIn ? 'not-allowed' : 'pointer',
|
||||||
src={ProfileIcon}
|
"&:disabled": {
|
||||||
alt="Profile"
|
opacity: 0.3,
|
||||||
style={{ width: "70%", height: "70%", objectFit: "contain" }}
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
/>
|
}
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ProfileIcon}
|
||||||
|
alt="Profile"
|
||||||
|
style={{ width: "70%", height: "70%", objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
</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() ? (
|
||||||
<div
|
terminals.map((terminal) => (
|
||||||
key={terminal.id}
|
<div
|
||||||
className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${
|
|
||||||
splitTabIds.includes(terminal.id) || activeTab === terminal.id ? "block" : "hidden"
|
|
||||||
} flex-1`}
|
|
||||||
style={{
|
|
||||||
order: splitTabIds.includes(terminal.id)
|
|
||||||
? splitTabIds.indexOf(terminal.id)
|
|
||||||
: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NewTerminal
|
|
||||||
key={terminal.id}
|
key={terminal.id}
|
||||||
hostConfig={terminal.hostConfig}
|
className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${
|
||||||
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
|
splitTabIds.includes(terminal.id) || activeTab === terminal.id ? "block" : "hidden"
|
||||||
setIsNoAuthHidden={setIsNoAuthHidden}
|
} flex-1`}
|
||||||
ref={(ref) => {
|
style={{
|
||||||
terminal.terminalRef = ref;
|
order: splitTabIds.includes(terminal.id)
|
||||||
|
? splitTabIds.indexOf(terminal.id)
|
||||||
|
: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<NewTerminal
|
||||||
|
key={terminal.id}
|
||||||
|
hostConfig={terminal.hostConfig}
|
||||||
|
isVisible={activeTab === terminal.id || splitTabIds.includes(terminal.id)}
|
||||||
|
setIsNoAuthHidden={setIsNoAuthHidden}
|
||||||
|
ref={(ref) => {
|
||||||
|
terminal.terminalRef = ref;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center text-neutral-400">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Welcome to Termix</h2>
|
||||||
|
<p>{isLoggingIn ? "Checking login status..." : "Please login to start managing your SSH connections"}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
<NoAuthenticationModal
|
<NoAuthenticationModal
|
||||||
isHidden={isNoAuthHidden}
|
isHidden={isNoAuthHidden}
|
||||||
form={authForm}
|
form={authForm}
|
||||||
@@ -488,85 +611,108 @@ function App() {
|
|||||||
handleAuthSubmit={handleAuthSubmit}
|
handleAuthSubmit={handleAuthSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
<AddHostModal
|
{userRef.current?.getUser() && (
|
||||||
isHidden={isAddHostHidden}
|
<>
|
||||||
form={addHostForm}
|
<AddHostModal
|
||||||
setForm={setAddHostForm}
|
isHidden={isAddHostHidden}
|
||||||
handleAddHost={handleAddHost}
|
form={addHostForm}
|
||||||
setIsAddHostHidden={setIsAddHostHidden}
|
setForm={setAddHostForm}
|
||||||
/>
|
handleAddHost={handleAddHost}
|
||||||
<EditHostModal
|
setIsAddHostHidden={setIsAddHostHidden}
|
||||||
isHidden={isEditHostHidden}
|
/>
|
||||||
form={editHostForm}
|
<EditHostModal
|
||||||
setForm={setEditHostForm}
|
isHidden={isEditHostHidden}
|
||||||
handleEditHost={handleEditHost}
|
form={editHostForm}
|
||||||
setIsEditHostHidden={setIsEditHostHidden}
|
setForm={setEditHostForm}
|
||||||
hostConfig={currentHostConfig}
|
handleEditHost={handleEditHost}
|
||||||
/>
|
setIsEditHostHidden={setIsEditHostHidden}
|
||||||
<CreateUserModal
|
hostConfig={currentHostConfig}
|
||||||
isHidden={isCreateUserHidden}
|
/>
|
||||||
form={createUserForm}
|
<ProfileModal
|
||||||
setForm={setCreateUserForm}
|
isHidden={isProfileHidden}
|
||||||
handleCreateUser={handleCreateUser}
|
getUser={getUser}
|
||||||
setIsCreateUserHidden={setIsCreateUserHidden}
|
handleDeleteUser={handleDeleteUser}
|
||||||
setIsLoginUserHidden={setIsLoginUserHidden}
|
handleLogoutUser={handleLogoutUser}
|
||||||
/>
|
setIsProfileHidden={setIsProfileHidden}
|
||||||
<ProfileModal
|
/>
|
||||||
isHidden={isProfileHidden}
|
{isLaunchpadOpen && (
|
||||||
getUser={getUser}
|
<Launchpad
|
||||||
handleDeleteUser={handleDeleteUser}
|
onClose={() => setIsLaunchpadOpen(false)}
|
||||||
handleLogoutUser={handleLogoutUser}
|
getHosts={getHosts}
|
||||||
setIsProfileHidden={setIsProfileHidden}
|
connectToHost={connectToHostWithConfig}
|
||||||
/>
|
isAddHostHidden={isAddHostHidden}
|
||||||
<ErrorModal
|
setIsAddHostHidden={setIsAddHostHidden}
|
||||||
isHidden={isErrorHidden}
|
isEditHostHidden={isEditHostHidden}
|
||||||
errorMessage={errorMessage}
|
isErrorHidden={isErrorHidden}
|
||||||
setIsErrorHidden={setIsErrorHidden}
|
deleteHost={deleteHost}
|
||||||
/>
|
editHost={handleEditHost}
|
||||||
{isLaunchpadOpen && (
|
/>
|
||||||
<Launchpad
|
)}
|
||||||
onClose={() => setIsLaunchpadOpen(false)}
|
</>
|
||||||
getHosts={getHosts}
|
)}
|
||||||
connectToHost={connectToHostWithConfig}
|
|
||||||
isAddHostHidden={isAddHostHidden}
|
<ErrorModal
|
||||||
setIsAddHostHidden={setIsAddHostHidden}
|
isHidden={isErrorHidden}
|
||||||
isEditHostHidden={isEditHostHidden}
|
errorMessage={errorMessage}
|
||||||
isErrorHidden={isErrorHidden}
|
setIsErrorHidden={setIsErrorHidden}
|
||||||
deleteHost={deleteHost}
|
|
||||||
editHost={updateEditHostForm}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<LoginUserModal
|
<LoginUserModal
|
||||||
isHidden={isLoginUserHidden}
|
isHidden={isLoginUserHidden}
|
||||||
form={loginUserForm}
|
form={loginUserForm}
|
||||||
setForm={setLoginUserForm}
|
setForm={setLoginUserForm}
|
||||||
handleLoginUser={handleLoginUser}
|
handleLoginUser={handleLoginUser}
|
||||||
handleGuestLogin={handleGuestLogin}
|
handleGuestLogin={handleGuestLogin}
|
||||||
setIsLoginUserHidden={setIsLoginUserHidden}
|
setIsLoginUserHidden={setIsLoginUserHidden}
|
||||||
setIsCreateUserHidden={setIsCreateUserHidden}
|
setIsCreateUserHidden={setIsCreateUserHidden}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* User component */}
|
<CreateUserModal
|
||||||
<User
|
isHidden={isCreateUserHidden}
|
||||||
ref={userRef}
|
form={createUserForm}
|
||||||
onLoginSuccess={() => setIsLoginUserHidden(true)}
|
setForm={setCreateUserForm}
|
||||||
onCreateSuccess={() => {
|
handleCreateUser={handleCreateUser}
|
||||||
setIsCreateUserHidden(true);
|
setIsCreateUserHidden={setIsCreateUserHidden}
|
||||||
handleLoginUser({ username: createUserForm.username, password: createUserForm.password })}
|
setIsLoginUserHidden={setIsLoginUserHidden}
|
||||||
}
|
/>
|
||||||
onDeleteSuccess={() => {
|
|
||||||
setIsProfileHidden(true);
|
{/* User component */}
|
||||||
window.location.reload();
|
<User
|
||||||
}}
|
ref={userRef}
|
||||||
onFailure={(error) => {
|
onLoginSuccess={() => {
|
||||||
setErrorMessage(`Action failed: ${error}`);
|
setIsLoginUserHidden(true);
|
||||||
setIsErrorHidden(false);
|
setIsLoggingIn(false);
|
||||||
}}
|
setIsErrorHidden(true);
|
||||||
/>
|
}}
|
||||||
|
onCreateSuccess={() => {
|
||||||
|
setIsCreateUserHidden(true);
|
||||||
|
handleLoginUser({
|
||||||
|
username: createUserForm.username,
|
||||||
|
password: createUserForm.password,
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsLoginUserHidden(true);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
setIsErrorHidden(true);
|
||||||
|
},
|
||||||
|
onFailure: (error) => {
|
||||||
|
setErrorMessage(`Login failed: ${error}`);
|
||||||
|
setIsErrorHidden(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onDeleteSuccess={() => {
|
||||||
|
setIsProfileHidden(true);
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
onFailure={(error) => {
|
||||||
|
setErrorMessage(`Action failed: ${error}`);
|
||||||
|
setIsErrorHidden(false);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CssVarsProvider>
|
</CssVarsProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,20 +4,19 @@ 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({
|
||||||
getHosts,
|
onClose,
|
||||||
connectToHost,
|
getHosts,
|
||||||
isAddHostHidden,
|
connectToHost,
|
||||||
setIsAddHostHidden,
|
isAddHostHidden,
|
||||||
isEditHostHidden,
|
setIsAddHostHidden,
|
||||||
isErrorHidden,
|
isEditHostHidden,
|
||||||
deleteHost,
|
isErrorHidden,
|
||||||
editHost,
|
deleteHost,
|
||||||
}) {
|
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}
|
||||||
|
onDrop={handleDropOnNoFolder}
|
||||||
|
>
|
||||||
|
{noFolder.map((host) => renderHostItem(host))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
{/* Render folders and their hosts */}
|
||||||
className="text-black"
|
{sortedFolders.map((folderName) => (
|
||||||
onClick={() => connectToHost(hostConfig)}
|
<div key={folderName} className="mb-2">
|
||||||
sx={{
|
<div
|
||||||
backgroundColor: "#6e6e6e",
|
className={`flex items-center gap-2 p-2 bg-neutral-600 rounded-lg cursor-pointer hover:bg-neutral-500 transition-colors ${
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
isDraggingOver === folderName ? 'bg-neutral-500 border-2 border-dashed border-neutral-400' : ''
|
||||||
}}
|
}`}
|
||||||
>
|
onClick={() => toggleFolder(folderName)}
|
||||||
Connect
|
onDragOver={(e) => handleDragOver(e, folderName)}
|
||||||
</Button>
|
onDragLeave={handleDragLeave}
|
||||||
<Button
|
onDrop={(e) => handleDrop(e, folderName)}
|
||||||
className="text-black"
|
>
|
||||||
onClick={() => {
|
<span className={`font-bold w-4 text-center transition-transform ${collapsedFolders.has(folderName) ? 'rotate-[-90deg]' : ''}`}>
|
||||||
deleteHost({ ...hostConfig, _id: hostWrapper._id });
|
▼
|
||||||
}}
|
</span>
|
||||||
sx={{
|
<span className="font-bold">{folderName}</span>
|
||||||
backgroundColor: "#6e6e6e",
|
<span className="text-sm text-gray-300">
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
({grouped[folderName].length})
|
||||||
}}
|
</span>
|
||||||
>
|
</div>
|
||||||
Delete
|
{!collapsedFolders.has(folderName) && (
|
||||||
</Button>
|
<div className="ml-6 mt-2 flex flex-col gap-2">
|
||||||
<Button
|
{grouped[folderName].map((host) => renderHostItem(host))}
|
||||||
className="text-black"
|
</div>
|
||||||
onClick={() => {
|
)}
|
||||||
editHost(hostConfig);
|
</div>
|
||||||
}}
|
))}
|
||||||
sx={{
|
</>
|
||||||
backgroundColor: "#6e6e6e",
|
|
||||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</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,172 +86,258 @@ 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
|
||||||
<FormControl>
|
value={activeTab}
|
||||||
<FormLabel>Host Name</FormLabel>
|
onChange={(e, val) => setActiveTab(val)}
|
||||||
<Input
|
sx={{
|
||||||
value={form.name}
|
backgroundColor: theme.palette.general.disabled,
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
borderRadius: '8px',
|
||||||
sx={{
|
padding: '8px',
|
||||||
backgroundColor: theme.palette.general.primary,
|
marginBottom: '16px',
|
||||||
color: theme.palette.text.primary,
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</FormControl>
|
<TabList
|
||||||
<FormControl error={!form.ip}>
|
sx={{
|
||||||
<FormLabel>Host IP</FormLabel>
|
width: '100%',
|
||||||
<Input
|
gap: 0,
|
||||||
value={form.ip}
|
mb: 2,
|
||||||
onChange={(e) => setForm({ ...form, ip: e.target.value })}
|
'& button': {
|
||||||
required
|
flex: 1,
|
||||||
sx={{
|
bgcolor: 'transparent',
|
||||||
backgroundColor: theme.palette.general.primary,
|
color: theme.palette.text.secondary,
|
||||||
color: theme.palette.text.primary,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl error={!form.user}>
|
|
||||||
<FormLabel>Host User</FormLabel>
|
|
||||||
<Input
|
|
||||||
value={form.user}
|
|
||||||
onChange={(e) => setForm({ ...form, user: e.target.value })}
|
|
||||||
required
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
|
|
||||||
<FormLabel>Authentication Method</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={form.authMethod || 'Select Auth'}
|
|
||||||
onChange={(e, newValue) => setForm({ ...form, authMethod: newValue })}
|
|
||||||
required
|
|
||||||
sx={{
|
|
||||||
backgroundColor: !form.authMethod || form.authMethod === 'Select Auth' ? theme.palette.general.tertiary : theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.general.disabled,
|
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
||||||
},
|
},
|
||||||
}}
|
'&.Mui-selected': {
|
||||||
>
|
bgcolor: theme.palette.general.primary,
|
||||||
<Option value="Select Auth" disabled>
|
color: theme.palette.text.primary,
|
||||||
Select Auth
|
'&:hover': {
|
||||||
</Option>
|
bgcolor: theme.palette.general.primary,
|
||||||
<Option value="password">Password</Option>
|
},
|
||||||
<Option value="rsaKey">RSA Key</Option>
|
},
|
||||||
</Select>
|
},
|
||||||
</FormControl>
|
}}
|
||||||
{form.authMethod === 'password' && (
|
>
|
||||||
<FormControl error={!form.password}>
|
<Tab>Basic Info</Tab>
|
||||||
<FormLabel>Host Password</FormLabel>
|
<Tab>Connection</Tab>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<Tab>Authentication</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanel value={0}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Host Name</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
type={showPassword ? 'text' : 'password'}
|
value={form.name}
|
||||||
value={form.password}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Folder</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.folder || ''}
|
||||||
|
onChange={(e) => setForm({ ...form, folder: e.target.value })}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={1}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl error={!form.ip}>
|
||||||
|
<FormLabel>Host IP</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.ip}
|
||||||
|
onChange={(e) => setForm({ ...form, ip: e.target.value })}
|
||||||
required
|
required
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
flex: 1,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
</FormControl>
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
<FormControl error={!form.user}>
|
||||||
|
<FormLabel>Host User</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.user}
|
||||||
|
onChange={(e) => setForm({ ...form, user: e.target.value })}
|
||||||
|
required
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl error={form.port < 1 || form.port > 65535}>
|
||||||
|
<FormLabel>Host Port</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={form.port}
|
||||||
|
onChange={(e) => setForm({ ...form, port: e.target.value })}
|
||||||
|
min={1}
|
||||||
|
max={65535}
|
||||||
|
required
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={2}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Remember Host</FormLabel>
|
||||||
|
<Checkbox
|
||||||
|
checked={form.rememberHost}
|
||||||
|
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
marginLeft: 1,
|
'&.Mui-checked': {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
</FormControl>
|
||||||
</IconButton>
|
{form.rememberHost && (
|
||||||
</div>
|
<>
|
||||||
</FormControl>
|
<FormControl>
|
||||||
)}
|
<FormLabel>Store Password</FormLabel>
|
||||||
{form.authMethod === 'rsaKey' && (
|
<Checkbox
|
||||||
<FormControl error={!form.rsaKey}>
|
checked={form.storePassword}
|
||||||
<FormLabel>RSA Key</FormLabel>
|
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
|
||||||
<Input
|
sx={{
|
||||||
type="file"
|
color: theme.palette.text.primary,
|
||||||
onChange={handleFileChange}
|
'&.Mui-checked': {
|
||||||
required
|
color: theme.palette.text.primary,
|
||||||
sx={{
|
},
|
||||||
backgroundColor: theme.palette.general.primary,
|
}}
|
||||||
color: theme.palette.text.primary,
|
/>
|
||||||
padding: 1,
|
</FormControl>
|
||||||
textAlign: 'center',
|
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
|
||||||
width: '100%',
|
<FormLabel>Authentication Method</FormLabel>
|
||||||
}}
|
<Select
|
||||||
/>
|
value={form.authMethod || 'Select Auth'}
|
||||||
</FormControl>
|
onChange={(e, newValue) => setForm({ ...form, authMethod: newValue })}
|
||||||
)}
|
required
|
||||||
<FormControl error={form.port < 1 || form.port > 65535}>
|
sx={{
|
||||||
<FormLabel>Host Port</FormLabel>
|
backgroundColor: !form.authMethod || form.authMethod === 'Select Auth' ? theme.palette.general.tertiary : theme.palette.general.primary,
|
||||||
<Input
|
color: theme.palette.text.primary,
|
||||||
value={form.port}
|
'&:hover': {
|
||||||
onChange={(e) => setForm({ ...form, port: e.target.value })}
|
backgroundColor: theme.palette.general.disabled,
|
||||||
min={1}
|
},
|
||||||
max={65535}
|
}}
|
||||||
required
|
>
|
||||||
sx={{
|
<Option value="Select Auth" disabled>
|
||||||
backgroundColor: theme.palette.general.primary,
|
Select Auth
|
||||||
color: theme.palette.text.primary,
|
</Option>
|
||||||
}}
|
<Option value="password">Password</Option>
|
||||||
/>
|
<Option value="rsaKey">RSA Key</Option>
|
||||||
</FormControl>
|
</Select>
|
||||||
<FormControl>
|
</FormControl>
|
||||||
<FormLabel>Remember Host</FormLabel>
|
{form.authMethod === 'password' && (
|
||||||
<Checkbox
|
<FormControl error={!form.password}>
|
||||||
checked={form.rememberHost}
|
<FormLabel>Host Password</FormLabel>
|
||||||
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
sx={{
|
<Input
|
||||||
color: theme.palette.text.primary,
|
type={showPassword ? 'text' : 'password'}
|
||||||
'&.Mui-checked': {
|
value={form.password}
|
||||||
color: theme.palette.text.primary,
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||||
},
|
required
|
||||||
}}
|
sx={{
|
||||||
/>
|
backgroundColor: theme.palette.general.primary,
|
||||||
</FormControl>
|
color: theme.palette.text.primary,
|
||||||
{form.rememberHost && (
|
flex: 1,
|
||||||
<FormControl>
|
}}
|
||||||
<FormLabel>Store Password</FormLabel>
|
/>
|
||||||
<Checkbox
|
<IconButton
|
||||||
checked={form.storePassword}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
|
sx={{
|
||||||
sx={{
|
color: theme.palette.text.primary,
|
||||||
color: theme.palette.text.primary,
|
marginLeft: 1,
|
||||||
'&.Mui-checked': {
|
}}
|
||||||
color: theme.palette.text.primary,
|
>
|
||||||
},
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
}}
|
</IconButton>
|
||||||
/>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
<Button
|
{form.authMethod === 'rsaKey' && (
|
||||||
type="submit"
|
<FormControl error={!form.rsaKey}>
|
||||||
disabled={!isFormValid()}
|
<FormLabel>RSA Key</FormLabel>
|
||||||
sx={{
|
<Button
|
||||||
backgroundColor: theme.palette.general.primary,
|
component="label"
|
||||||
'&:hover': {
|
sx={{
|
||||||
backgroundColor: theme.palette.general.disabled,
|
backgroundColor: theme.palette.general.primary,
|
||||||
},
|
color: theme.palette.text.primary,
|
||||||
}}
|
width: '100%',
|
||||||
>
|
display: 'flex',
|
||||||
Add Host
|
justifyContent: 'center',
|
||||||
</Button>
|
alignItems: 'center',
|
||||||
</Stack>
|
height: '40px',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{form.rsaKey ? 'Change RSA Key File' : 'Upload RSA Key File'}
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
required
|
||||||
|
sx={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isFormValid()}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
'&:disabled': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
},
|
||||||
|
marginTop: 3,
|
||||||
|
width: '100%',
|
||||||
|
height: '40px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Host
|
||||||
|
</Button>
|
||||||
</form>
|
</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,35 +78,42 @@ 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
|
||||||
sx={{
|
open={!isHidden}
|
||||||
overflowX: 'hidden',
|
onClose={() => setIsEditHostHidden(true)}
|
||||||
display: 'flex',
|
sx={{
|
||||||
justifyContent: 'center',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ModalDialog
|
<ModalDialog
|
||||||
layout="center"
|
layout="center"
|
||||||
@@ -114,169 +123,249 @@ 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
|
||||||
<FormControl>
|
value={activeTab}
|
||||||
<FormLabel>Host Name</FormLabel>
|
onChange={(e, val) => setActiveTab(val)}
|
||||||
<Input
|
sx={{
|
||||||
value={form.name}
|
backgroundColor: theme.palette.general.disabled,
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
borderRadius: '8px',
|
||||||
sx={{
|
padding: '8px',
|
||||||
backgroundColor: theme.palette.general.primary,
|
marginBottom: '16px',
|
||||||
color: theme.palette.text.primary
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</FormControl>
|
<TabList
|
||||||
|
sx={{
|
||||||
<FormControl error={!form.ip}>
|
width: '100%',
|
||||||
<FormLabel>Host IP</FormLabel>
|
gap: 0,
|
||||||
<Input
|
mb: 2,
|
||||||
value={form.ip}
|
'& button': {
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, ip: e.target.value }))}
|
flex: 1,
|
||||||
sx={{
|
bgcolor: 'transparent',
|
||||||
backgroundColor: theme.palette.general.primary,
|
color: theme.palette.text.secondary,
|
||||||
color: theme.palette.text.primary
|
'&:hover': {
|
||||||
}}
|
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
||||||
/>
|
},
|
||||||
</FormControl>
|
'&.Mui-selected': {
|
||||||
|
bgcolor: theme.palette.general.primary,
|
||||||
<FormControl error={!form.user}>
|
|
||||||
<FormLabel>Host User</FormLabel>
|
|
||||||
<Input
|
|
||||||
value={form.user}
|
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, user: e.target.value }))}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
{form.storePassword && form.authMethod !== 'Select Auth' && (
|
|
||||||
<FormControl error={form.authMethod === 'Select Auth'}>
|
|
||||||
<FormLabel>Authentication Method</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={form.authMethod}
|
|
||||||
onChange={(e, val) => handleAuthChange(val)}
|
|
||||||
sx={{
|
|
||||||
backgroundColor:
|
|
||||||
form.authMethod === 'Select Auth'
|
|
||||||
? theme.palette.general.tertiary
|
|
||||||
: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.general.disabled
|
bgcolor: theme.palette.general.primary,
|
||||||
}
|
},
|
||||||
}}
|
},
|
||||||
>
|
},
|
||||||
<Option value="Select Auth" disabled>Select Auth</Option>
|
|
||||||
<Option value="password">Password</Option>
|
|
||||||
<Option value="rsaKey">RSA Key</Option>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{form.authMethod === 'password' && form.storePassword && (
|
|
||||||
<FormControl error={!form.password}>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
<Input
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
value={form.password}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((prev) => ({ ...prev, password: e.target.value }))
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
flex: 1
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
marginLeft: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{form.authMethod === 'rsaKey' && form.storePassword && (
|
|
||||||
<FormControl
|
|
||||||
error={!form.rsaKey && !hostConfig?.rsaKey}
|
|
||||||
sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
|
|
||||||
>
|
|
||||||
<FormLabel>RSA Key</FormLabel>
|
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
alignItems: 'center'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{hostConfig?.rsaKey && !form.rsaKey && (
|
|
||||||
<FormLabel sx={{ color: theme.palette.text.secondary }}>
|
|
||||||
Existing key detected. Upload to replace.
|
|
||||||
</FormLabel>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormControl error={form.port < 1 || form.port > 65535}>
|
|
||||||
<FormLabel>Host Port</FormLabel>
|
|
||||||
<Input
|
|
||||||
value={form.port}
|
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>Store Password</FormLabel>
|
|
||||||
<Checkbox
|
|
||||||
checked={form.storePassword}
|
|
||||||
onChange={(e) => handleStorePasswordChange(e.target.checked)}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
'&.Mui-checked': {
|
|
||||||
color: theme.palette.text.primary
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isFormValid()}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.general.primary,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.general.disabled
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save Changes
|
<Tab>Basic Info</Tab>
|
||||||
</Button>
|
<Tab>Connection</Tab>
|
||||||
</Stack>
|
<Tab>Authentication</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanel value={0}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Host Name</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.name}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Folder</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.folder}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, folder: e.target.value }))}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={1}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl error={!form.ip}>
|
||||||
|
<FormLabel>Host IP</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.ip}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, ip: e.target.value }))}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl error={form.port < 1 || form.port > 65535}>
|
||||||
|
<FormLabel>Host Port</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={form.port}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl error={!form.user}>
|
||||||
|
<FormLabel>Host User</FormLabel>
|
||||||
|
<Input
|
||||||
|
value={form.user}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, user: e.target.value }))}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={2}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Store Password</FormLabel>
|
||||||
|
<Checkbox
|
||||||
|
checked={form.storePassword}
|
||||||
|
onChange={(e) => handleStorePasswordChange(e.target.checked)}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&.Mui-checked': {
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{form.storePassword && (
|
||||||
|
<FormControl error={form.authMethod === 'Select Auth'}>
|
||||||
|
<FormLabel>Authentication Method</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={form.authMethod}
|
||||||
|
onChange={(e, val) => handleAuthChange(val)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Option value="Select Auth" disabled>Select Auth</Option>
|
||||||
|
<Option value="password">Password</Option>
|
||||||
|
<Option value="rsaKey">RSA Key</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.authMethod === 'password' && form.storePassword && (
|
||||||
|
<FormControl error={!form.password}>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={form.password}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
flex: 1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
marginLeft: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.authMethod === 'rsaKey' && form.storePassword && (
|
||||||
|
<FormControl error={!form.rsaKey && !hostConfig?.rsaKey}>
|
||||||
|
<FormLabel>RSA Key</FormLabel>
|
||||||
|
<Button
|
||||||
|
component="label"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '40px',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{form.rsaKey ? 'Change RSA Key File' : 'Upload RSA Key File'}
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
sx={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
{hostConfig?.rsaKey && !form.rsaKey && (
|
||||||
|
<FormLabel
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
mt: 1,
|
||||||
|
display: 'block',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Existing key detected. Upload to replace.
|
||||||
|
</FormLabel>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isFormValid()}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled
|
||||||
|
},
|
||||||
|
'&:disabled': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
},
|
||||||
|
marginTop: 3,
|
||||||
|
width: '100%',
|
||||||
|
height: '40px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
</form>
|
</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",
|
||||||
backgroundColor: theme.palette.general.tertiary,
|
justifyContent: "center",
|
||||||
borderColor: theme.palette.general.secondary,
|
alignItems: "center",
|
||||||
color: theme.palette.text.primary,
|
}}
|
||||||
padding: 3,
|
>
|
||||||
borderRadius: 10,
|
<div style={{
|
||||||
width: "auto",
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
maxWidth: "90vw",
|
borderColor: theme.palette.general.secondary,
|
||||||
minWidth: "fit-content",
|
borderWidth: "1px",
|
||||||
overflow: "hidden",
|
borderStyle: "solid",
|
||||||
display: "flex",
|
borderRadius: "0.5rem",
|
||||||
flexDirection: "column",
|
width: "400px",
|
||||||
alignItems: "center",
|
overflow: "hidden",
|
||||||
justifyContent: "center",
|
}}>
|
||||||
gap: 1,
|
<div className="p-4 flex flex-col gap-4">
|
||||||
}}
|
<Button
|
||||||
>
|
fullWidth
|
||||||
<DialogTitle
|
onClick={handleLogoutUser}
|
||||||
|
startDecorator={<LogoutIcon />}
|
||||||
sx={{
|
sx={{
|
||||||
marginBottom: 1.5,
|
backgroundColor: theme.palette.general.tertiary,
|
||||||
backgroundColor: theme.palette.general.primary,
|
color: "white",
|
||||||
color: theme.palette.text.primary,
|
"&:hover": {
|
||||||
padding: 1,
|
backgroundColor: theme.palette.general.secondary,
|
||||||
borderRadius: 10,
|
},
|
||||||
width: "100%",
|
height: "40px",
|
||||||
textAlign: "center",
|
border: `1px solid ${theme.palette.general.secondary}`,
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
User: {getUserName()}
|
Logout
|
||||||
</DialogTitle>
|
</Button>
|
||||||
<DialogContent sx={{ width: "100%" }}>
|
|
||||||
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden", mt: 1.5 }}>
|
<Button
|
||||||
<Button
|
fullWidth
|
||||||
onClick={handleDelete}
|
color="danger"
|
||||||
sx={{
|
onClick={() => {
|
||||||
backgroundColor: theme.palette.general.primary,
|
if (window.confirm("Are you sure you want to delete your account? This action cannot be undone.")) {
|
||||||
'&:hover': {
|
handleDeleteUser({
|
||||||
backgroundColor: theme.palette.general.disabled,
|
onSuccess: () => setIsProfileHidden(true),
|
||||||
},
|
onFailure: (error) => console.error(error),
|
||||||
width: "100%",
|
});
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
Delete User
|
startDecorator={<DeleteForeverIcon />}
|
||||||
</Button>
|
sx={{
|
||||||
<Button
|
backgroundColor: "#c53030",
|
||||||
onClick={handleLogout}
|
color: "white",
|
||||||
sx={{
|
"&:hover": {
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: "#9b2c2c",
|
||||||
'&:hover': {
|
},
|
||||||
backgroundColor: theme.palette.general.disabled,
|
height: "40px",
|
||||||
},
|
border: "1px solid #9b2c2c",
|
||||||
width: "100%",
|
}}
|
||||||
}}
|
>
|
||||||
>
|
Delete Account
|
||||||
Logout
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</Stack>
|
</div>
|
||||||
</DialogContent>
|
</Modal>
|
||||||
</ModalDialog>
|
|
||||||
</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