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

View File

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

View File

@@ -1,9 +1,22 @@
import PropTypes from "prop-types";
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";
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 [filteredHosts, setFilteredHosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
@@ -15,6 +28,24 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
const [isDeleting, setIsDeleting] = useState(false);
const [isShareModalHidden, setIsShareModalHidden] = useState(true);
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 () => {
try {
@@ -51,9 +82,9 @@ 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()) ||
hostConfig.folder?.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]);
@@ -168,7 +199,7 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
const handleDelete = async (e, hostWrapper) => {
e.stopPropagation();
if (isDeleting) return;
setIsDeleting(true);
try {
const isOwner = hostWrapper.createdBy?._id === userRef.current?.getUser()?.id;
@@ -229,7 +260,8 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
</div>
<div className="flex gap-2">
<Button
className="text-black"
variant="outlined"
className="text-white"
onClick={(e) => {
e.stopPropagation();
if (!hostWrapper.config || !hostWrapper.config.ip || !hostWrapper.config.user) {
@@ -242,76 +274,36 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
cursor: isDeleting ? "not-allowed" : "pointer",
borderColor: "#3d3d3d",
borderWidth: "2px",
color: "#fff",
}}
>
Connect
</Button>
{isOwner && (
<>
<Button
className="text-black"
onClick={(e) => {
e.stopPropagation();
setSelectedHostForShare(hostWrapper);
setIsShareModalHidden(false);
}}
disabled={isDeleting}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer"
}}
>
Share
</Button>
<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>
)}
<IconButton
variant="outlined"
className="text-white"
onClick={(e) => {
e.stopPropagation();
setSelectedHost(hostWrapper);
setIsMenuOpen(!isMenuOpen);
anchorEl.current = e.currentTarget;
}}
disabled={isDeleting}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" },
opacity: isDeleting ? 0.5 : 1,
cursor: isDeleting ? "not-allowed" : "pointer",
borderColor: "#3d3d3d",
borderWidth: "2px",
color: "#fff",
}}
>
</IconButton>
</div>
</div>
);
@@ -352,7 +344,6 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
return (
<>
{/* 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')}
@@ -362,7 +353,6 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
{noFolder.map((host) => renderHostItem(host))}
</div>
{/* Render folders and their hosts */}
{sortedFolders.map((folderName) => (
<div key={folderName} className="mb-2">
<div
@@ -403,6 +393,68 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
handleShare={handleShare}
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>
);
}
@@ -418,6 +470,8 @@ HostViewer.propTypes = {
onModalOpen: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
userRef: PropTypes.object.isRequired,
isMenuOpen: PropTypes.bool.isRequired,
setIsMenuOpen: PropTypes.func.isRequired,
};
export default HostViewer;

View File

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

View File

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