Improve host viewer UI, improve performance of SSH. Prep for release.
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 {
|
||||||
@@ -51,9 +82,9 @@ 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()) ||
|
return hostConfig.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
hostConfig.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
hostConfig.folder?.toLowerCase().includes(searchTerm.toLowerCase());
|
hostConfig.folder?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
});
|
});
|
||||||
setFilteredHosts(filtered);
|
setFilteredHosts(filtered);
|
||||||
}, [searchTerm, hosts]);
|
}, [searchTerm, hosts]);
|
||||||
@@ -168,7 +199,7 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
|
|||||||
const handleDelete = async (e, hostWrapper) => {
|
const handleDelete = async (e, hostWrapper) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (isDeleting) return;
|
if (isDeleting) return;
|
||||||
|
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
try {
|
try {
|
||||||
const isOwner = hostWrapper.createdBy?._id === userRef.current?.getUser()?.id;
|
const isOwner = hostWrapper.createdBy?._id === userRef.current?.getUser()?.id;
|
||||||
@@ -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();
|
setSelectedHost(hostWrapper);
|
||||||
setSelectedHostForShare(hostWrapper);
|
setIsMenuOpen(!isMenuOpen);
|
||||||
setIsShareModalHidden(false);
|
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",
|
||||||
Share
|
color: "#fff",
|
||||||
</Button>
|
}}
|
||||||
<Button
|
>
|
||||||
className="text-black"
|
⋮
|
||||||
onClick={(e) => handleDelete(e, hostWrapper)}
|
</IconButton>
|
||||||
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;
|
||||||
@@ -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'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user