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:
Karmaa
2025-03-16 01:51:14 -05:00
parent fd966b9954
commit fda8c7ce4b
7 changed files with 1162 additions and 692 deletions

View File

@@ -31,6 +31,7 @@ function App() {
const [nextId, setNextId] = useState(1);
const [addHostForm, setAddHostForm] = useState({
name: "",
folder: "",
ip: "",
user: "",
password: "",
@@ -41,6 +42,7 @@ function App() {
});
const [editHostForm, setEditHostForm] = useState({
name: "",
folder: "",
ip: "",
user: "",
password: "",
@@ -66,6 +68,7 @@ function App() {
const [splitTabIds, setSplitTabIds] = useState([]);
const [isEditHostHidden, setIsEditHostHidden] = useState(true);
const [currentHostConfig, setCurrentHostConfig] = useState(null);
const [isLoggingIn, setIsLoggingIn] = useState(true);
useEffect(() => {
const handleKeyDown = (e) => {
@@ -124,23 +127,97 @@ function App() {
useEffect(() => {
const sessionToken = localStorage.getItem('sessionToken');
if (sessionToken) {
setTimeout(() => {
handleLoginUser({
let isComponentMounted = true;
let isLoginInProgress = false;
if (userRef.current?.getUser()) {
setIsLoggingIn(false);
setIsLoginUserHidden(true);
return;
}
if (!sessionToken) {
setIsLoggingIn(false);
setIsLoginUserHidden(false);
return;
}
setIsLoggingIn(true);
let loginAttempts = 0;
const maxAttempts = 50;
let attemptLoginInterval;
const loginTimeout = setTimeout(() => {
if (isComponentMounted) {
clearInterval(attemptLoginInterval);
if (!userRef.current?.getUser()) {
localStorage.removeItem('sessionToken');
setIsLoginUserHidden(false);
setIsLoggingIn(false);
setErrorMessage('Login timed out. Please try again.');
setIsErrorHidden(false);
}
}
}, 10000);
const attemptLogin = () => {
if (!isComponentMounted || isLoginInProgress) return;
if (loginAttempts >= maxAttempts || userRef.current?.getUser()) {
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
if (!userRef.current?.getUser()) {
localStorage.removeItem('sessionToken');
setIsLoginUserHidden(false);
setIsLoggingIn(false);
setErrorMessage('Login timed out. Please try again.');
setIsErrorHidden(false);
}
return;
}
if (userRef.current) {
isLoginInProgress = true;
userRef.current.loginUser({
sessionToken,
onSuccess: () => {
if (isComponentMounted) {
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
setIsLoginUserHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
}
isLoginInProgress = false;
},
onFailure: (error) => {
if (isComponentMounted) {
if (!userRef.current?.getUser()) {
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
localStorage.removeItem('sessionToken');
setErrorMessage(`Auto-login failed: ${error}`);
setIsErrorHidden(false);
setIsLoginUserHidden(false);
setIsLoggingIn(false);
}
}
isLoginInProgress = false;
},
});
}, 500);
} else {
setIsLoginUserHidden(false);
}
loginAttempts++;
};
attemptLoginInterval = setInterval(attemptLogin, 100);
attemptLogin();
return () => {
isComponentMounted = false;
clearTimeout(loginTimeout);
clearInterval(attemptLoginInterval);
};
}, []);
const handleAddHost = () => {
@@ -168,6 +245,8 @@ function App() {
id: nextId,
title: addHostForm.name || addHostForm.ip,
hostConfig: {
name: addHostForm.name,
folder: addHostForm.folder,
ip: addHostForm.ip,
user: addHostForm.user,
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
@@ -180,7 +259,7 @@ function App() {
setActiveTab(nextId);
setNextId(nextId + 1);
setIsAddHostHidden(true);
setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" });
setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth", rememberHost: false, storePassword: true });
}
const handleAuthSubmit = (form) => {
@@ -217,6 +296,7 @@ function App() {
const handleSaveHost = () => {
let hostConfig = {
name: addHostForm.name || addHostForm.ip,
folder: addHostForm.folder,
ip: addHostForm.ip,
user: addHostForm.user,
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
@@ -235,15 +315,32 @@ function App() {
if (sessionToken) {
userRef.current.loginUser({
sessionToken,
onSuccess,
onFailure,
onSuccess: () => {
setIsLoginUserHidden(true);
setIsLoggingIn(false);
if (onSuccess) onSuccess();
},
onFailure: (error) => {
localStorage.removeItem('sessionToken');
setIsLoginUserHidden(false);
setIsLoggingIn(false);
if (onFailure) onFailure(error);
},
});
} else {
userRef.current.loginUser({
username,
password,
onSuccess,
onFailure,
onSuccess: () => {
setIsLoginUserHidden(true);
setIsLoggingIn(false);
if (onSuccess) onSuccess();
},
onFailure: (error) => {
setIsLoginUserHidden(false);
setIsLoggingIn(false);
if (onFailure) onFailure(error);
},
});
}
}
@@ -264,7 +361,7 @@ function App() {
onFailure,
});
}
}
};
const handleDeleteUser = ({ onSuccess, onFailure }) => {
if (userRef.current) {
@@ -311,31 +408,21 @@ function App() {
}
};
const handleEditHost = async () => {
const handleEditHost = async (oldConfig, newConfig = null) => {
try {
// Only clear the password if switching to RSA or storePassword is false
if (editHostForm.authMethod === 'rsaKey') {
editHostForm.password = '';
} else if (!editHostForm.storePassword) {
editHostForm.password = '';
}
if (newConfig) {
await userRef.current.editHost({
oldHostConfig: currentHostConfig,
newHostConfig: editHostForm,
oldHostConfig: oldConfig,
newHostConfig: newConfig,
});
// Refresh the updated config
const refreshedHosts = await userRef.current.getAllHosts();
const updated = refreshedHosts.find(
(h) => h.config.ip === editHostForm.ip && h.config.user === editHostForm.user
);
if (updated) {
setCurrentHostConfig(updated.config);
return;
}
setIsEditHostHidden(true);
updateEditHostForm(oldConfig);
} catch (error) {
alert('Edit failed: ' + error);
console.error('Edit failed:', error);
setErrorMessage(`Edit failed: ${error}`);
setIsErrorHidden(false);
}
};
@@ -398,8 +485,11 @@ function App() {
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-4">
{/* Launchpad Button */}
<Button
disabled={isLoggingIn || !userRef.current?.getUser()}
onClick={() => setIsLaunchpadOpen(true)}
sx={{
backgroundColor: theme.palette.general.tertiary,
@@ -408,33 +498,21 @@ function App() {
height: "52px",
width: "52px",
padding: 0,
opacity: (!userRef.current?.getUser() || isLoggingIn) ? 0.3 : 1,
cursor: (!userRef.current?.getUser() || isLoggingIn) ? 'not-allowed' : 'pointer',
"&:disabled": {
opacity: 0.3,
backgroundColor: theme.palette.general.tertiary,
}
}}
>
<img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70", objectFit: "contain" }} />
<img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70%", objectFit: "contain" }} />
</Button>
{/* Add Host Button */}
<Button
disabled={isLoggingIn || !userRef.current?.getUser()}
onClick={() => setIsAddHostHidden(false)}
sx={{
backgroundColor: theme.palette.general.tertiary,
"&:hover": { backgroundColor: theme.palette.general.secondary },
flexShrink: 0,
height: "52px",
width: "52px",
fontSize: "3.5rem",
display: "flex",
justifyContent: "center",
alignItems: "center",
paddingTop: "2px",
}}
>
+
</Button>
{/* Profile Button */}
<Button
onClick={() => setIsProfileHidden(false)}
sx={{
backgroundColor: theme.palette.general.tertiary,
"&:hover": { backgroundColor: theme.palette.general.secondary },
@@ -445,6 +523,41 @@ function App() {
justifyContent: "center",
alignItems: "center",
padding: 0,
opacity: (!userRef.current?.getUser() || isLoggingIn) ? 0.3 : 1,
cursor: (!userRef.current?.getUser() || isLoggingIn) ? 'not-allowed' : 'pointer',
"&:disabled": {
opacity: 0.3,
backgroundColor: theme.palette.general.tertiary,
},
fontSize: "4rem",
fontWeight: "600",
lineHeight: "0",
paddingBottom: "8px",
}}
>
+
</Button>
{/* Profile Button */}
<Button
disabled={isLoggingIn}
onClick={() => userRef.current?.getUser() ? setIsProfileHidden(false) : setIsLoginUserHidden(false)}
sx={{
backgroundColor: theme.palette.general.tertiary,
"&:hover": { backgroundColor: theme.palette.general.secondary },
flexShrink: 0,
height: "52px",
width: "52px",
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: 0,
opacity: isLoggingIn ? 0.3 : 1,
cursor: isLoggingIn ? 'not-allowed' : 'pointer',
"&:disabled": {
opacity: 0.3,
backgroundColor: theme.palette.general.tertiary,
}
}}
>
<img
@@ -454,10 +567,12 @@ function App() {
/>
</Button>
</div>
</div>
{/* Terminal Views */}
<div className={`relative p-4 terminal-container ${getLayoutStyle()}`}>
{terminals.map((terminal) => (
{userRef.current?.getUser() ? (
terminals.map((terminal) => (
<div
key={terminal.id}
className={`bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 ${
@@ -479,7 +594,15 @@ function App() {
}}
/>
</div>
))}
))
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center text-neutral-400">
<h2 className="text-2xl font-bold mb-4">Welcome to Termix</h2>
<p>{isLoggingIn ? "Checking login status..." : "Please login to start managing your SSH connections"}</p>
</div>
</div>
)}
<NoAuthenticationModal
isHidden={isNoAuthHidden}
form={authForm}
@@ -488,9 +611,10 @@ function App() {
handleAuthSubmit={handleAuthSubmit}
/>
</div>
</div>
{/* Modals */}
{userRef.current?.getUser() && (
<>
<AddHostModal
isHidden={isAddHostHidden}
form={addHostForm}
@@ -506,14 +630,6 @@ function App() {
setIsEditHostHidden={setIsEditHostHidden}
hostConfig={currentHostConfig}
/>
<CreateUserModal
isHidden={isCreateUserHidden}
form={createUserForm}
setForm={setCreateUserForm}
handleCreateUser={handleCreateUser}
setIsCreateUserHidden={setIsCreateUserHidden}
setIsLoginUserHidden={setIsLoginUserHidden}
/>
<ProfileModal
isHidden={isProfileHidden}
getUser={getUser}
@@ -521,11 +637,6 @@ function App() {
handleLogoutUser={handleLogoutUser}
setIsProfileHidden={setIsProfileHidden}
/>
<ErrorModal
isHidden={isErrorHidden}
errorMessage={errorMessage}
setIsErrorHidden={setIsErrorHidden}
/>
{isLaunchpadOpen && (
<Launchpad
onClose={() => setIsLaunchpadOpen(false)}
@@ -536,9 +647,17 @@ function App() {
isEditHostHidden={isEditHostHidden}
isErrorHidden={isErrorHidden}
deleteHost={deleteHost}
editHost={updateEditHostForm}
editHost={handleEditHost}
/>
)}
</>
)}
<ErrorModal
isHidden={isErrorHidden}
errorMessage={errorMessage}
setIsErrorHidden={setIsErrorHidden}
/>
<LoginUserModal
isHidden={isLoginUserHidden}
@@ -550,14 +669,39 @@ function App() {
setIsCreateUserHidden={setIsCreateUserHidden}
/>
<CreateUserModal
isHidden={isCreateUserHidden}
form={createUserForm}
setForm={setCreateUserForm}
handleCreateUser={handleCreateUser}
setIsCreateUserHidden={setIsCreateUserHidden}
setIsLoginUserHidden={setIsLoginUserHidden}
/>
{/* User component */}
<User
ref={userRef}
onLoginSuccess={() => setIsLoginUserHidden(true)}
onLoginSuccess={() => {
setIsLoginUserHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
}}
onCreateSuccess={() => {
setIsCreateUserHidden(true);
handleLoginUser({ username: createUserForm.username, password: createUserForm.password })}
handleLoginUser({
username: createUserForm.username,
password: createUserForm.password,
onSuccess: () => {
setIsLoginUserHidden(true);
setIsLoggingIn(false);
setIsErrorHidden(true);
},
onFailure: (error) => {
setErrorMessage(`Login failed: ${error}`);
setIsErrorHidden(false);
}
});
}}
onDeleteSuccess={() => {
setIsProfileHidden(true);
window.location.reload();
@@ -565,9 +709,11 @@ function App() {
onFailure={(error) => {
setErrorMessage(`Action failed: ${error}`);
setIsErrorHidden(false);
setIsLoggingIn(false);
}}
/>
</div>
</div>
</CssVarsProvider>
);
}

View File

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

View File

@@ -2,11 +2,14 @@ import PropTypes from "prop-types";
import { useState, useEffect, useRef } from "react";
import { Button, Input } from "@mui/joy";
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost }) {
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost, openEditPanel }) {
const [hosts, setHosts] = useState([]);
const [filteredHosts, setFilteredHosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [collapsedFolders, setCollapsedFolders] = useState(new Set());
const [draggedHost, setDraggedHost] = useState(null);
const [isDraggingOver, setIsDraggingOver] = useState(null);
const isMounted = useRef(true);
const fetchHosts = async () => {
@@ -44,11 +47,181 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
useEffect(() => {
const filtered = hosts.filter((hostWrapper) => {
const hostConfig = hostWrapper.config || {};
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) || hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase());
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
hostConfig.folder?.toLowerCase().includes(searchTerm.toLowerCase());
});
setFilteredHosts(filtered);
}, [searchTerm, hosts]);
const toggleFolder = (folderName) => {
setCollapsedFolders(prev => {
const newSet = new Set(prev);
if (newSet.has(folderName)) {
newSet.delete(folderName);
} else {
newSet.add(folderName);
}
return newSet;
});
};
const groupHostsByFolder = (hosts) => {
const grouped = {};
const noFolder = [];
const sortedHosts = [...hosts].sort((a, b) => {
const nameA = (a.config?.name || a.config?.ip || '').toLowerCase();
const nameB = (b.config?.name || b.config?.ip || '').toLowerCase();
return nameA.localeCompare(nameB);
});
sortedHosts.forEach(host => {
const folder = host.config?.folder;
if (folder) {
if (!grouped[folder]) {
grouped[folder] = [];
}
grouped[folder].push(host);
} else {
noFolder.push(host);
}
});
const sortedFolders = Object.keys(grouped).sort((a, b) => a.localeCompare(b));
return { grouped, sortedFolders, noFolder };
};
const handleDragStart = (e, host) => {
setDraggedHost(host);
e.dataTransfer.setData('text/plain', '');
};
const handleDragOver = (e, folderName) => {
e.preventDefault();
setIsDraggingOver(folderName);
};
const handleDragLeave = () => {
setIsDraggingOver(null);
};
const handleDrop = async (e, targetFolder) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingOver(null);
if (!draggedHost) return;
if (draggedHost.config.folder === targetFolder) return;
const newConfig = {
...draggedHost.config,
folder: targetFolder
};
try {
await editHost(draggedHost.config, newConfig);
await fetchHosts();
} catch (error) {
console.error('Failed to update folder:', error);
}
setDraggedHost(null);
};
const handleDropOnNoFolder = async (e) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingOver(null);
if (!draggedHost || !draggedHost.config.folder) return;
const newConfig = {
...draggedHost.config,
folder: null
};
try {
await editHost(draggedHost.config, newConfig);
await fetchHosts();
} catch (error) {
console.error('Failed to remove from folder:', error);
}
setDraggedHost(null);
};
const renderHostItem = (hostWrapper) => {
const hostConfig = hostWrapper.config || {};
if (!hostConfig) {
return null;
}
return (
<div
key={hostWrapper._id}
className={`flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 w-full cursor-grab active:cursor-grabbing hover:border-neutral-500 transition-colors ${draggedHost === hostWrapper ? 'opacity-50' : ''}`}
draggable
onDragStart={(e) => handleDragStart(e, hostWrapper)}
onDragEnd={() => setDraggedHost(null)}
>
<div className="flex items-center gap-2 flex-1">
<div className="text-neutral-500 cursor-grab active:cursor-grabbing"></div>
<div>
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p>
<p className="text-sm text-gray-400">
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`}
</p>
</div>
</div>
<div className="flex gap-2">
<Button
className="text-black"
onClick={(e) => {
e.stopPropagation();
connectToHost(hostConfig);
}}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Connect
</Button>
<Button
className="text-black"
onClick={(e) => {
e.stopPropagation();
deleteHost({ ...hostConfig, _id: hostWrapper._id });
}}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Delete
</Button>
<Button
className="text-black"
onClick={(e) => {
e.stopPropagation();
openEditPanel(hostConfig);
}}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Edit
</Button>
</div>
</div>
);
};
return (
<div className="h-full w-full p-4 text-white flex flex-col">
<div className="flex items-center justify-between mb-2 w-full gap-2">
@@ -79,60 +252,51 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
<p className="text-gray-300">Loading hosts...</p>
) : filteredHosts.length > 0 ? (
<div className="flex flex-col gap-2 w-full">
{filteredHosts.map((hostWrapper, index) => {
const hostConfig = hostWrapper.config || {};
if (!hostConfig) {
return null;
}
{(() => {
const { grouped, sortedFolders, noFolder } = groupHostsByFolder(filteredHosts);
return (
<div key={index} className="flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 w-full">
<div>
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p>
<p className="text-sm text-gray-400">
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`}
</p>
</div>
<div className="flex gap-2">
<Button
className="text-black"
onClick={() => connectToHost(hostConfig)}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
<>
{/* Render hosts without folders first */}
<div
className={`flex flex-col gap-2 p-2 rounded-lg transition-colors ${isDraggingOver === 'no-folder' ? 'bg-neutral-700' : ''}`}
onDragOver={(e) => handleDragOver(e, 'no-folder')}
onDragLeave={handleDragLeave}
onDrop={handleDropOnNoFolder}
>
Connect
</Button>
<Button
className="text-black"
onClick={() => {
deleteHost({ ...hostConfig, _id: hostWrapper._id });
}}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Delete
</Button>
<Button
className="text-black"
onClick={() => {
editHost(hostConfig);
}}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
}}
>
Edit
</Button>
{noFolder.map((host) => renderHostItem(host))}
</div>
{/* Render folders and their hosts */}
{sortedFolders.map((folderName) => (
<div key={folderName} className="mb-2">
<div
className={`flex items-center gap-2 p-2 bg-neutral-600 rounded-lg cursor-pointer hover:bg-neutral-500 transition-colors ${
isDraggingOver === folderName ? 'bg-neutral-500 border-2 border-dashed border-neutral-400' : ''
}`}
onClick={() => toggleFolder(folderName)}
onDragOver={(e) => handleDragOver(e, folderName)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, folderName)}
>
<span className={`font-bold w-4 text-center transition-transform ${collapsedFolders.has(folderName) ? 'rotate-[-90deg]' : ''}`}>
</span>
<span className="font-bold">{folderName}</span>
<span className="text-sm text-gray-300">
({grouped[folderName].length})
</span>
</div>
{!collapsedFolders.has(folderName) && (
<div className="ml-6 mt-2 flex flex-col gap-2">
{grouped[folderName].map((host) => renderHostItem(host))}
</div>
)}
</div>
))}
</>
);
})}
})()}
</div>
) : (
<p className="text-gray-300">No hosts available...</p>
@@ -148,6 +312,7 @@ HostViewer.propTypes = {
setIsAddHostHidden: PropTypes.func.isRequired,
deleteHost: PropTypes.func.isRequired,
editHost: PropTypes.func.isRequired,
openEditPanel: PropTypes.func.isRequired,
};
export default HostViewer;

View File

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

View File

@@ -13,7 +13,11 @@ import {
Select,
Option,
Checkbox,
IconButton
IconButton,
Tabs,
TabList,
Tab,
TabPanel
} from '@mui/joy';
import theme from '/src/theme';
import { useState } from 'react';
@@ -22,6 +26,7 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
const [showPassword, setShowPassword] = useState(false);
const [activeTab, setActiveTab] = useState(0);
const handleFileChange = (e) => {
const file = e.target.files[0];
@@ -68,7 +73,6 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
<CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsAddHostHidden(true)}
sx={{
overflow: 'hidden',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
@@ -82,17 +86,57 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
color: theme.palette.text.primary,
padding: 3,
borderRadius: 10,
maxWidth: '400px',
maxWidth: '500px',
width: '100%',
overflow: 'hidden',
maxHeight: '80vh',
overflow: 'auto',
boxSizing: 'border-box',
mx: 2,
}}
>
<DialogTitle>Add Host</DialogTitle>
<DialogTitle sx={{ mb: 2 }}>Add Host</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmit}>
<Stack spacing={2} sx={{ width: '100%' }}>
<Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}
sx={{
backgroundColor: theme.palette.general.disabled,
borderRadius: '8px',
padding: '8px',
marginBottom: '16px',
width: '100%',
}}
>
<TabList
sx={{
width: '100%',
gap: 0,
mb: 2,
'& button': {
flex: 1,
bgcolor: 'transparent',
color: theme.palette.text.secondary,
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.1)',
},
'&.Mui-selected': {
bgcolor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.primary,
},
},
},
}}
>
<Tab>Basic Info</Tab>
<Tab>Connection</Tab>
<Tab>Authentication</Tab>
</TabList>
<TabPanel value={0}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Host Name</FormLabel>
<Input
@@ -104,6 +148,22 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
}}
/>
</FormControl>
<FormControl>
<FormLabel>Folder</FormLabel>
<Input
value={form.folder || ''}
onChange={(e) => setForm({ ...form, folder: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={1}>
<Stack spacing={2}>
<FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel>
<Input
@@ -128,6 +188,54 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
}}
/>
</FormControl>
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
type="number"
value={form.port}
onChange={(e) => setForm({ ...form, port: e.target.value })}
min={1}
max={65535}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={2}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Remember Host</FormLabel>
<Checkbox
checked={form.rememberHost}
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
{form.rememberHost && (
<>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel>
<Select
@@ -179,75 +287,57 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
{form.authMethod === 'rsaKey' && (
<FormControl error={!form.rsaKey}>
<FormLabel>RSA Key</FormLabel>
<Input
type="file"
onChange={handleFileChange}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
padding: 1,
textAlign: 'center',
width: '100%',
}}
/>
</FormControl>
)}
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
value={form.port}
onChange={(e) => setForm({ ...form, port: e.target.value })}
min={1}
max={65535}
required
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Remember Host</FormLabel>
<Checkbox
checked={form.rememberHost}
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
{form.rememberHost && (
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
)}
<Button
type="submit"
disabled={!isFormValid()}
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.rsaKey ? 'Change RSA Key File' : 'Upload RSA Key File'}
<Input
type="file"
onChange={handleFileChange}
required
sx={{ display: 'none' }}
/>
</Button>
</FormControl>
)}
</>
)}
</Stack>
</TabPanel>
</Tabs>
<Button
type="submit"
disabled={!isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
'&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)',
},
marginTop: 3,
width: '100%',
height: '40px',
}}
>
Add Host
</Button>
</Stack>
</form>
</DialogContent>
</ModalDialog>
@@ -260,6 +350,7 @@ AddHostModal.propTypes = {
isHidden: PropTypes.bool.isRequired,
form: PropTypes.shape({
name: PropTypes.string,
folder: PropTypes.string,
ip: PropTypes.string.isRequired,
user: PropTypes.string.isRequired,
password: PropTypes.string,

View File

@@ -14,7 +14,11 @@ import {
Select,
Option,
IconButton,
Checkbox
Checkbox,
Tabs,
TabList,
Tab,
TabPanel
} from '@mui/joy';
import theme from '/src/theme';
import Visibility from '@mui/icons-material/Visibility';
@@ -22,25 +26,23 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => {
const [showPassword, setShowPassword] = useState(false);
const [activeTab, setActiveTab] = useState(0);
useEffect(() => {
if (hostConfig) {
const storePassword = hostConfig.password || hostConfig.rsaKey;
if (hostConfig && !isHidden) {
setForm({
...form,
name: hostConfig.name || '',
ip: hostConfig.ip || '',
user: hostConfig.user || '',
password: storePassword && hostConfig.password ? hostConfig.password : '',
rsaKey: '',
port: Number(hostConfig.port) || 22,
authMethod: hostConfig.rsaKey ? 'rsaKey' : (storePassword ? 'password' : 'Select Auth'),
rememberHost: hostConfig.rememberHost || true,
storePassword: storePassword ?? false
name: hostConfig.name || "",
folder: hostConfig.folder || "",
ip: hostConfig.ip || "",
user: hostConfig.user || "",
password: hostConfig.password || "",
port: hostConfig.port || 22,
authMethod: hostConfig.password ? "password" : hostConfig.rsaKey ? "rsaKey" : "Select Auth",
rememberHost: true,
storePassword: true,
});
}
}, [hostConfig, setForm]);
}, [hostConfig, isHidden]);
const handleFileChange = (e) => {
const file = e.target.files[0];
@@ -65,7 +67,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
const handleStorePasswordChange = (checked) => {
setForm((prev) => ({
...prev,
storePassword: checked,
storePassword: Boolean(checked),
authMethod: checked ? 'password' : 'Select Auth'
}));
};
@@ -76,31 +78,38 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
const portNum = Number(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
if (storePassword && authMethod === 'password' && !password.trim()) return false;
if (storePassword && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false;
if (storePassword && authMethod === 'Select Auth') return false;
if (Boolean(storePassword) && authMethod === 'password' && !password?.trim()) return false;
if (Boolean(storePassword) && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false;
if (Boolean(storePassword) && authMethod === 'Select Auth') return false;
return true;
};
const handleSubmit = (e) => {
const handleSave = async (e) => {
e.preventDefault();
if (isFormValid()) {
const { authMethod, password, rsaKey, storePassword, ...rest } = form;
handleEditHost({
...rest,
authMethod,
password: authMethod === 'password' && storePassword ? password : '',
rsaKey: authMethod === 'rsaKey' ? rsaKey : ''
});
try {
const newConfig = {
...form,
port: String(form.port),
};
if (form.authMethod === 'rsaKey' || !form.storePassword) {
newConfig.password = '';
}
await handleEditHost(hostConfig, newConfig);
setIsEditHostHidden(true);
} catch (error) {
console.error('Failed to save:', error);
}
};
return (
<CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsEditHostHidden(true)}
<Modal
open={!isHidden}
onClose={() => setIsEditHostHidden(true)}
sx={{
overflowX: 'hidden',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
@@ -114,17 +123,57 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
color: theme.palette.text.primary,
padding: 3,
borderRadius: 10,
maxWidth: '400px',
maxWidth: '500px',
width: '100%',
overflow: 'hidden',
maxHeight: '80vh',
overflow: 'auto',
boxSizing: 'border-box',
mx: 2,
}}
>
<DialogTitle>Edit Host</DialogTitle>
<DialogTitle sx={{ mb: 2 }}>Edit Host</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmit}>
<Stack spacing={2} sx={{ width: '100%', overflow: 'hidden' }}>
<form onSubmit={handleSave}>
<Tabs
value={activeTab}
onChange={(e, val) => setActiveTab(val)}
sx={{
backgroundColor: theme.palette.general.disabled,
borderRadius: '8px',
padding: '8px',
marginBottom: '16px',
width: '100%',
}}
>
<TabList
sx={{
width: '100%',
gap: 0,
mb: 2,
'& button': {
flex: 1,
bgcolor: 'transparent',
color: theme.palette.text.secondary,
'&:hover': {
bgcolor: 'rgba(255, 255, 255, 0.1)',
},
'&.Mui-selected': {
bgcolor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.primary,
},
},
},
}}
>
<Tab>Basic Info</Tab>
<Tab>Connection</Tab>
<Tab>Authentication</Tab>
</TabList>
<TabPanel value={0}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Host Name</FormLabel>
<Input
@@ -137,6 +186,22 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
/>
</FormControl>
<FormControl>
<FormLabel>Folder</FormLabel>
<Input
value={form.folder}
onChange={(e) => setForm((prev) => ({ ...prev, folder: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={1}>
<Stack spacing={2}>
<FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel>
<Input
@@ -149,6 +214,19 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
/>
</FormControl>
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
type="number"
value={form.port}
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
<FormControl error={!form.user}>
<FormLabel>Host User</FormLabel>
<Input
@@ -160,22 +238,34 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
}}
/>
</FormControl>
</Stack>
</TabPanel>
{form.storePassword && form.authMethod !== 'Select Auth' && (
<TabPanel value={2}>
<Stack spacing={2}>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => handleStorePasswordChange(e.target.checked)}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary
}
}}
/>
</FormControl>
{form.storePassword && (
<FormControl error={form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod}
onChange={(e, val) => handleAuthChange(val)}
sx={{
backgroundColor:
form.authMethod === 'Select Auth'
? theme.palette.general.tertiary
: theme.palette.general.primary,
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled
}
}}
>
<Option value="Select Auth" disabled>Select Auth</Option>
@@ -192,9 +282,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) =>
setForm((prev) => ({ ...prev, password: e.target.value }))
}
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
@@ -215,53 +303,48 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
)}
{form.authMethod === 'rsaKey' && form.storePassword && (
<FormControl
error={!form.rsaKey && !hostConfig?.rsaKey}
sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
>
<FormControl error={!form.rsaKey && !hostConfig?.rsaKey}>
<FormLabel>RSA Key</FormLabel>
<Input
type="file"
onChange={handleFileChange}
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
alignItems: 'center'
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.rsaKey ? 'Change RSA Key File' : 'Upload RSA Key File'}
<Input
type="file"
onChange={handleFileChange}
sx={{ display: 'none' }}
/>
</Button>
{hostConfig?.rsaKey && !form.rsaKey && (
<FormLabel sx={{ color: theme.palette.text.secondary }}>
<FormLabel
sx={{
color: theme.palette.text.secondary,
fontSize: '0.875rem',
mt: 1,
display: 'block',
textAlign: 'center'
}}
>
Existing key detected. Upload to replace.
</FormLabel>
)}
</FormControl>
)}
<FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel>
<Input
value={form.port}
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary
}}
/>
</FormControl>
<FormControl>
<FormLabel>Store Password</FormLabel>
<Checkbox
checked={form.storePassword}
onChange={(e) => handleStorePasswordChange(e.target.checked)}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary
}
}}
/>
</FormControl>
</Stack>
</TabPanel>
</Tabs>
<Button
type="submit"
@@ -271,12 +354,18 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled
}
},
'&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)',
},
marginTop: 3,
width: '100%',
height: '40px',
}}
>
Save Changes
</Button>
</Stack>
</form>
</DialogContent>
</ModalDialog>

View File

@@ -1,101 +1,83 @@
import PropTypes from 'prop-types';
import { CssVarsProvider } from '@mui/joy/styles';
import { Modal, Button, DialogTitle, DialogContent, ModalDialog, Stack } from '@mui/joy';
import theme from '/src/theme';
const ProfileModal = ({ isHidden, getUser, handleDeleteUser, handleLogoutUser, setIsProfileHidden }) => {
const handleDelete = () => {
handleDeleteUser({
onSuccess: () => {
window.location.reload();
}
});
};
const handleLogout = () => {
handleLogoutUser({
onSuccess: () => {
window.location.reload();
}
});
}
const getUserName = () => {
const user = getUser();
return user ? user.username : '';
}
import { Modal, Typography, Button } from "@mui/joy";
import LogoutIcon from "@mui/icons-material/Logout";
import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import theme from "../theme";
export default function ProfileModal({
isHidden,
getUser,
handleDeleteUser,
handleLogoutUser,
setIsProfileHidden,
}) {
return (
<CssVarsProvider theme={theme}>
<Modal open={!isHidden} onClose={() => setIsProfileHidden(true)}>
<ModalDialog
layout="center"
<Modal
open={!isHidden}
onClose={() => setIsProfileHidden(true)}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<div style={{
backgroundColor: theme.palette.general.tertiary,
borderColor: theme.palette.general.secondary,
color: theme.palette.text.primary,
padding: 3,
borderRadius: 10,
width: "auto",
maxWidth: "90vw",
minWidth: "fit-content",
borderWidth: "1px",
borderStyle: "solid",
borderRadius: "0.5rem",
width: "400px",
overflow: "hidden",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 1,
}}
>
<DialogTitle
sx={{
marginBottom: 1.5,
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
padding: 1,
borderRadius: 10,
width: "100%",
textAlign: "center",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
User: {getUserName()}
</DialogTitle>
<DialogContent sx={{ width: "100%" }}>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden", mt: 1.5 }}>
}}>
<div className="p-4 flex flex-col gap-4">
<Button
onClick={handleDelete}
fullWidth
onClick={handleLogoutUser}
startDecorator={<LogoutIcon />}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
backgroundColor: theme.palette.general.tertiary,
color: "white",
"&:hover": {
backgroundColor: theme.palette.general.secondary,
},
width: "100%",
}}
>
Delete User
</Button>
<Button
onClick={handleLogout}
sx={{
backgroundColor: theme.palette.general.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
width: "100%",
height: "40px",
border: `1px solid ${theme.palette.general.secondary}`,
}}
>
Logout
</Button>
</Stack>
</DialogContent>
</ModalDialog>
<Button
fullWidth
color="danger"
onClick={() => {
if (window.confirm("Are you sure you want to delete your account? This action cannot be undone.")) {
handleDeleteUser({
onSuccess: () => setIsProfileHidden(true),
onFailure: (error) => console.error(error),
});
}
}}
startDecorator={<DeleteForeverIcon />}
sx={{
backgroundColor: "#c53030",
color: "white",
"&:hover": {
backgroundColor: "#9b2c2c",
},
height: "40px",
border: "1px solid #9b2c2c",
}}
>
Delete Account
</Button>
</div>
</div>
</Modal>
</CssVarsProvider>
);
};
}
ProfileModal.propTypes = {
isHidden: PropTypes.bool.isRequired,
@@ -104,5 +86,3 @@ ProfileModal.propTypes = {
handleLogoutUser: PropTypes.func.isRequired,
setIsProfileHidden: PropTypes.func.isRequired,
};
export default ProfileModal;