Improve host viewer UI, improve performance of SSH. Prep for release.

This commit is contained in:
Karmaa
2025-03-21 00:37:21 -05:00
parent 679e4fc751
commit 8209fb5318
5 changed files with 146 additions and 101 deletions

View File

@@ -70,6 +70,7 @@ function App() {
const [currentHostConfig, setCurrentHostConfig] = useState(null); const [currentHostConfig, setCurrentHostConfig] = useState(null);
const [isLoggingIn, setIsLoggingIn] = useState(true); const [isLoggingIn, setIsLoggingIn] = useState(true);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [isHostViewerMenuOpen, setIsHostViewerMenuOpen] = useState(null);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e) => { const handleKeyDown = (e) => {
@@ -694,6 +695,8 @@ function App() {
editHost={handleEditHost} editHost={handleEditHost}
shareHost={(hostId, username) => userRef.current?.shareHost(hostId, username)} shareHost={(hostId, username) => userRef.current?.shareHost(hostId, username)}
userRef={userRef} userRef={userRef}
isHostViewerMenuOpen={isHostViewerMenuOpen}
setIsHostViewerMenuOpen={setIsHostViewerMenuOpen}
/> />
)} )}
</> </>

View File

@@ -18,6 +18,8 @@ function Launchpad({
editHost, editHost,
shareHost, shareHost,
userRef, userRef,
isHostViewerMenuOpen,
setIsHostViewerMenuOpen,
}) { }) {
const launchpadRef = useRef(null); const launchpadRef = useRef(null);
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -32,6 +34,7 @@ function Launchpad({
isAddHostHidden && isAddHostHidden &&
isEditHostHidden && isEditHostHidden &&
isErrorHidden && isErrorHidden &&
!isHostViewerMenuOpen &&
!isAnyModalOpen !isAnyModalOpen
) { ) {
onClose(); onClose();
@@ -43,7 +46,7 @@ function Launchpad({
return () => { return () => {
document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("mousedown", handleClickOutside);
}; };
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden, isAnyModalOpen]); }, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden, isHostViewerMenuOpen, isAnyModalOpen]);
const handleModalOpen = () => { const handleModalOpen = () => {
setIsAnyModalOpen(true); setIsAnyModalOpen(true);
@@ -190,6 +193,8 @@ function Launchpad({
onModalOpen={handleModalOpen} onModalOpen={handleModalOpen}
onModalClose={handleModalClose} onModalClose={handleModalClose}
userRef={userRef} userRef={userRef}
isMenuOpen={isHostViewerMenuOpen || false}
setIsMenuOpen={setIsHostViewerMenuOpen}
/> />
)} )}
</div> </div>
@@ -211,6 +216,8 @@ Launchpad.propTypes = {
editHost: PropTypes.func.isRequired, editHost: PropTypes.func.isRequired,
shareHost: PropTypes.func.isRequired, shareHost: PropTypes.func.isRequired,
userRef: PropTypes.object.isRequired, userRef: PropTypes.object.isRequired,
isHostViewerMenuOpen: PropTypes.bool,
setIsHostViewerMenuOpen: PropTypes.func.isRequired,
}; };
export default Launchpad; export default Launchpad;

View File

@@ -1,9 +1,22 @@
import PropTypes from "prop-types"; 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, Menu, MenuItem, IconButton } from "@mui/joy";
import ShareHostModal from "../../modals/ShareHostModal"; import ShareHostModal from "../../modals/ShareHostModal";
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost, openEditPanel, shareHost, onModalOpen, onModalClose, userRef }) { function HostViewer({
getHosts,
connectToHost,
setIsAddHostHidden,
deleteHost,
editHost,
openEditPanel,
shareHost,
onModalOpen,
onModalClose,
userRef,
isMenuOpen,
setIsMenuOpen,
}) {
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);
@@ -15,6 +28,24 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [isShareModalHidden, setIsShareModalHidden] = useState(true); const [isShareModalHidden, setIsShareModalHidden] = useState(true);
const [selectedHostForShare, setSelectedHostForShare] = useState(null); const [selectedHostForShare, setSelectedHostForShare] = useState(null);
const [selectedHost, setSelectedHost] = useState(null);
const anchorEl = useRef(null);
const menuRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target) && anchorEl.current && !anchorEl.current.contains(event.target)) {
setIsMenuOpen(false);
setSelectedHost(null);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const fetchHosts = async () => { const fetchHosts = async () => {
try { try {
@@ -229,7 +260,8 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
className="text-black" variant="outlined"
className="text-white"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!hostWrapper.config || !hostWrapper.config.ip || !hostWrapper.config.user) { if (!hostWrapper.config || !hostWrapper.config.ip || !hostWrapper.config.user) {
@@ -242,76 +274,36 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
backgroundColor: "#6e6e6e", backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }, "&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1, opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer" cursor: isDeleting ? "not-allowed" : "pointer",
borderColor: "#3d3d3d",
borderWidth: "2px",
color: "#fff",
}} }}
> >
Connect Connect
</Button> </Button>
{isOwner && ( <IconButton
<> variant="outlined"
<Button className="text-white"
className="text-black"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setSelectedHostForShare(hostWrapper); setSelectedHost(hostWrapper);
setIsShareModalHidden(false); setIsMenuOpen(!isMenuOpen);
anchorEl.current = e.currentTarget;
}} }}
disabled={isDeleting} disabled={isDeleting}
sx={{ sx={{
backgroundColor: "#6e6e6e", backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }, "&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1, opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer" cursor: isDeleting ? "not-allowed" : "pointer",
borderColor: "#3d3d3d",
borderWidth: "2px",
color: "#fff",
}} }}
> >
Share
</Button> </IconButton>
<Button
className="text-black"
onClick={(e) => handleDelete(e, hostWrapper)}
disabled={isDeleting}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}}
>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
<Button
className="text-black"
onClick={(e) => {
e.stopPropagation();
openEditPanel(hostConfig);
}}
disabled={isDeleting}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}}
>
Edit
</Button>
</>
)}
{!isOwner && (
<Button
className="text-black"
onClick={(e) => handleDelete(e, hostWrapper)}
disabled={isDeleting}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}}
>
{isDeleting ? "Removing..." : "Remove Share"}
</Button>
)}
</div> </div>
</div> </div>
); );
@@ -352,7 +344,6 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
return ( return (
<> <>
{/* Render hosts without folders first */}
<div <div
className={`flex flex-col gap-2 p-2 rounded-lg transition-colors ${isDraggingOver === 'no-folder' ? 'bg-neutral-700' : ''}`} className={`flex flex-col gap-2 p-2 rounded-lg transition-colors ${isDraggingOver === 'no-folder' ? 'bg-neutral-700' : ''}`}
onDragOver={(e) => handleDragOver(e, 'no-folder')} onDragOver={(e) => handleDragOver(e, 'no-folder')}
@@ -362,7 +353,6 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
{noFolder.map((host) => renderHostItem(host))} {noFolder.map((host) => renderHostItem(host))}
</div> </div>
{/* Render folders and their hosts */}
{sortedFolders.map((folderName) => ( {sortedFolders.map((folderName) => (
<div key={folderName} className="mb-2"> <div key={folderName} className="mb-2">
<div <div
@@ -403,6 +393,68 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
handleShare={handleShare} handleShare={handleShare}
hostConfig={selectedHostForShare} hostConfig={selectedHostForShare}
/> />
<Menu
ref={menuRef}
anchorEl={anchorEl.current}
open={isMenuOpen}
onClose={() => {
setIsMenuOpen(false);
setSelectedHost(null);
}}
sx={{
"& .MuiMenu-list": {
backgroundColor: "#6e6e6e",
color: "white"
}
}}
>
{selectedHost && (
selectedHost.createdBy?._id === userRef.current?.getUser()?.id ? (
<>
<MenuItem
onClick={(e) => {
e.stopPropagation();
setSelectedHostForShare(selectedHost);
setIsShareModalHidden(false);
setIsMenuOpen(false);
}}
>
Share
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
openEditPanel(selectedHost.config);
setIsMenuOpen(false);
}}
>
Edit
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleDelete(e, selectedHost);
setIsMenuOpen(false);
}}
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete"}
</MenuItem>
</>
) : (
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleDelete(e, selectedHost);
setIsMenuOpen(false);
}}
disabled={isDeleting}
>
{isDeleting ? "Removing..." : "Remove Share"}
</MenuItem>
)
)}
</Menu>
</div> </div>
); );
} }
@@ -418,6 +470,8 @@ HostViewer.propTypes = {
onModalOpen: PropTypes.func.isRequired, onModalOpen: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
userRef: PropTypes.object.isRequired, userRef: PropTypes.object.isRequired,
isMenuOpen: PropTypes.bool.isRequired,
setIsMenuOpen: PropTypes.func.isRequired,
}; };
export default HostViewer; export default HostViewer;

View File

@@ -32,7 +32,7 @@ io.on("connection", (socket) => {
return; return;
} }
if (!hostConfig.password && !hostConfig.privateKey) { if (!hostConfig.password && !hostConfig.sshKey) {
logger.error("No authentication provided"); logger.error("No authentication provided");
socket.emit("error", "Authentication required"); socket.emit("error", "Authentication required");
return; return;
@@ -43,7 +43,6 @@ io.on("connection", (socket) => {
port: hostConfig.port, port: hostConfig.port,
user: hostConfig.user, user: hostConfig.user,
authType: hostConfig.password ? 'password' : 'key', authType: hostConfig.password ? 'password' : 'key',
keyType: hostConfig.keyType
}; };
logger.info("Connecting with config:", safeHostConfig); logger.info("Connecting with config:", safeHostConfig);
@@ -98,29 +97,11 @@ io.on("connection", (socket) => {
host: ip, host: ip,
port: port, port: port,
username: user, username: user,
password: password, password: password || undefined,
sshKey: sshKey ? Buffer.from(sshKey) : undefined, privateKey: sshKey ? Buffer.from(sshKey) : undefined,
tryKeyboard: true,
algorithms: { algorithms: {
kex: [ kex: ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256'],
'curve25519-sha256', serverHostKey: ['ssh-ed25519', 'ecdsa-sha2-nistp256']
'curve25519-sha256@libssh.org',
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
'diffie-hellman-group-exchange-sha256',
'diffie-hellman-group14-sha256',
'diffie-hellman-group14-sha1'
],
serverHostKey: [
'ssh-ed25519',
'ecdsa-sha2-nistp256',
'ecdsa-sha2-nistp384',
'ecdsa-sha2-nistp521',
'rsa-sha2-512',
'rsa-sha2-256',
'ssh-rsa'
]
} }
}); });
}); });

View File

@@ -442,7 +442,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
EditHostModal.propTypes = { EditHostModal.propTypes = {
isHidden: PropTypes.bool.isRequired, isHidden: PropTypes.bool.isRequired,
hostConfig: PropTypes.object.isRequired, hostConfig: PropTypes.object,
setIsEditHostHidden: PropTypes.func.isRequired, setIsEditHostHidden: PropTypes.func.isRequired,
handleEditHost: PropTypes.func.isRequired handleEditHost: PropTypes.func.isRequired
}; };