Dev 2.0 #23
92
src/App.jsx
92
src/App.jsx
@@ -1,8 +1,8 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { NewTerminal } from "./Terminal.jsx";
|
||||
import { User } from "./User.jsx";
|
||||
import AddHostModal from "./AddHostModal.jsx";
|
||||
import LoginUserModal from "./LoginUserModal.jsx";
|
||||
import AddHostModal from "./modals/AddHostModal.jsx";
|
||||
import LoginUserModal from "./modals/LoginUserModal.jsx";
|
||||
import { Button } from "@mui/joy";
|
||||
import { CssVarsProvider } from "@mui/joy";
|
||||
import theme from "./theme";
|
||||
@@ -12,9 +12,10 @@ import { Debounce } from './Utils';
|
||||
import TermixIcon from "./images/termix_icon.png";
|
||||
import RocketIcon from './images/launchpad_rocket.png';
|
||||
import ProfileIcon from './images/profile_icon.png';
|
||||
import CreateUserModal from "./CreateUserModal.jsx";
|
||||
import ProfileModal from "./ProfileModal.jsx";
|
||||
import ErrorModal from "./ErrorModal.jsx";
|
||||
import CreateUserModal from "./modals/CreateUserModal.jsx";
|
||||
import ProfileModal from "./modals/ProfileModal.jsx";
|
||||
import ErrorModal from "./modals/ErrorModal.jsx";
|
||||
import EditHostModal from "./modals/EditHostModal.jsx";
|
||||
|
||||
function App() {
|
||||
const [isAddHostHidden, setIsAddHostHidden] = useState(true);
|
||||
@@ -36,6 +37,15 @@ function App() {
|
||||
authMethod: "Select Auth",
|
||||
rememberHost: false,
|
||||
});
|
||||
const [editHostForm, setEditHostForm] = useState({
|
||||
name: "",
|
||||
ip: "",
|
||||
user: "",
|
||||
password: "",
|
||||
port: 22,
|
||||
authMethod: "Select Auth",
|
||||
rememberHost: true,
|
||||
});
|
||||
const [loginUserForm, setLoginUserForm] = useState({
|
||||
username: "",
|
||||
password: "",
|
||||
@@ -46,6 +56,8 @@ function App() {
|
||||
});
|
||||
const [isLaunchpadOpen, setIsLaunchpadOpen] = useState(false);
|
||||
const [splitTabIds, setSplitTabIds] = useState([]);
|
||||
const [isEditHostHidden, setIsEditHostHidden] = useState(true);
|
||||
const [currentHostConfig, setCurrentHostConfig] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
@@ -183,6 +195,23 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const createFolder = (folderName) => {
|
||||
if (userRef.current) {
|
||||
userRef.current.createFolder({
|
||||
folderName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const moveHostToFolder = (folderName, hostConfig) => {
|
||||
if (userRef.current) {
|
||||
userRef.current.moveHostToFolder({
|
||||
folderName,
|
||||
hostConfig,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => {
|
||||
if (userRef.current) {
|
||||
if (sessionToken) {
|
||||
@@ -241,6 +270,43 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteHost = (hostConfig) => {
|
||||
if (userRef.current) {
|
||||
userRef.current.deleteHost({
|
||||
hostConfig,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updateEditHostForm = (hostConfig) => {
|
||||
if (hostConfig) {
|
||||
setCurrentHostConfig(hostConfig);
|
||||
setIsEditHostHidden(false);
|
||||
} else {
|
||||
console.error("hostConfig is null");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditHost = () => {
|
||||
if (editHostForm.ip && editHostForm.user && ((editHostForm.authMethod === 'password' && editHostForm.password) || (editHostForm.authMethod === 'rsaKey' && editHostForm.rsaKey)) && editHostForm.port && editHostForm.authMethod !== 'Select Auth') {
|
||||
const user = getUser();
|
||||
editHostForm.rememberHost = true;
|
||||
|
||||
if (user && currentHostConfig) {
|
||||
userRef.current.editExistingHost({
|
||||
userId: user.id,
|
||||
oldHostConfig: currentHostConfig,
|
||||
newHostConfig: editHostForm,
|
||||
});
|
||||
setIsEditHostHidden(true);
|
||||
} else {
|
||||
console.error("User or currentHostConfig is null");
|
||||
}
|
||||
} else {
|
||||
alert("Please fill out all fields.");
|
||||
}
|
||||
};
|
||||
|
||||
const closeTab = (id) => {
|
||||
const newTerminals = terminals.filter((t) => t.id !== id);
|
||||
setTerminals(newTerminals);
|
||||
@@ -392,6 +458,14 @@ function App() {
|
||||
handleAddHost={handleAddHost}
|
||||
setIsAddHostHidden={setIsAddHostHidden}
|
||||
/>
|
||||
<EditHostModal
|
||||
isHidden={isEditHostHidden}
|
||||
form={editHostForm}
|
||||
setForm={setEditHostForm}
|
||||
handleEditHost={handleEditHost}
|
||||
setIsEditHostHidden={setIsEditHostHidden}
|
||||
hostConfig={currentHostConfig}
|
||||
/>
|
||||
<CreateUserModal
|
||||
isHidden={isCreateUserHidden}
|
||||
form={createUserForm}
|
||||
@@ -417,6 +491,14 @@ function App() {
|
||||
onClose={() => setIsLaunchpadOpen(false)}
|
||||
getHosts={getHosts}
|
||||
connectToHost={connectToHostWithConfig}
|
||||
isAddHostHidden={isAddHostHidden}
|
||||
setIsAddHostHidden={setIsAddHostHidden}
|
||||
isEditHostHidden={isEditHostHidden}
|
||||
isErrorHidden={isErrorHidden}
|
||||
deleteHost={deleteHost}
|
||||
editHost={updateEditHostForm}
|
||||
createFolder={createFolder}
|
||||
moveHostToFolder={moveHostToFolder}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Button } from "@mui/joy";
|
||||
|
||||
function HostViewer({ getHosts, connectToHost }) {
|
||||
const [hosts, setHosts] = useState([]);
|
||||
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
|
||||
async function fetchInitialHosts() {
|
||||
try {
|
||||
const savedHosts = await getHosts();
|
||||
if (isMounted.current) {
|
||||
setHosts(savedHosts || []);
|
||||
setInitialLoadComplete(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Initial host fetch failed:", error);
|
||||
if (isMounted.current) {
|
||||
setHosts([]);
|
||||
setInitialLoadComplete(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Immediate first fetch
|
||||
fetchInitialHosts();
|
||||
|
||||
// Periodic updates
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
const savedHosts = await getHosts();
|
||||
if (isMounted.current) {
|
||||
setHosts(savedHosts || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Periodic host update failed:", error);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [getHosts]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full p-4 text-white flex flex-col">
|
||||
<div className="flex items-center mb-2 w-full">
|
||||
<h2 className="text-lg font-bold">Saved Hosts</h2>
|
||||
</div>
|
||||
<div className="flex-grow overflow-auto">
|
||||
{!initialLoadComplete ? (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 animate-pulse">
|
||||
<div>
|
||||
<div className="h-5 bg-gray-600 rounded w-32 mb-2"></div>
|
||||
<div className="h-4 bg-gray-600 rounded w-24"></div>
|
||||
</div>
|
||||
<div className="h-8 w-24 bg-gray-600 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : hosts.length > 0 ? (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{hosts.map((hostWrapper, index) => {
|
||||
const hostConfig = hostWrapper.hostConfig || {};
|
||||
|
||||
const formattedHostConfig = {
|
||||
name: hostConfig.name || "Unknown Host Name",
|
||||
ip: hostConfig.ip || "Unknown IP",
|
||||
user: hostConfig.user || "Unknown User",
|
||||
password: hostConfig.password || undefined,
|
||||
rsaKey: hostConfig.rsaKey || undefined,
|
||||
port: hostConfig.port ? String(hostConfig.port) : "22",
|
||||
};
|
||||
|
||||
const displayName = hostConfig.name ? hostConfig.name : hostConfig.ip;
|
||||
|
||||
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">{displayName}</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : hostConfig.ip}:{hostConfig.port}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
connectToHost(formattedHostConfig);
|
||||
}}
|
||||
sx={{ backgroundColor: "#4CAF50", "&:hover": { backgroundColor: "#45A049" } }}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500">Hosts are loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
HostViewer.propTypes = {
|
||||
getHosts: PropTypes.func.isRequired,
|
||||
connectToHost: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default HostViewer;
|
||||
@@ -1,17 +1,40 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { CssVarsProvider } from '@mui/joy/styles';
|
||||
import { Button } from '@mui/joy';
|
||||
import HostViewerIcon from './images/host_viewer_icon.png';
|
||||
import theme from './theme';
|
||||
|
||||
// Apps
|
||||
import HostViewer from './Apps/HostViewer';
|
||||
import HostViewer from './apps/HostViewer';
|
||||
|
||||
function Launchpad({ onClose, getHosts, connectToHost }) {
|
||||
function Launchpad({
|
||||
onClose,
|
||||
getHosts,
|
||||
connectToHost,
|
||||
isAddHostHidden,
|
||||
setIsAddHostHidden,
|
||||
isEditHostHidden,
|
||||
isErrorHidden,
|
||||
deleteHost,
|
||||
editHost,
|
||||
createFolder,
|
||||
moveHostToFolder,
|
||||
}) {
|
||||
const launchpadRef = useRef(null);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [activeApp, setActiveApp] = useState('hostViewer');
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (launchpadRef.current && !launchpadRef.current.contains(event.target)) {
|
||||
// Close the launchpad when neither form is visible and no error is showing
|
||||
if (
|
||||
launchpadRef.current &&
|
||||
!launchpadRef.current.contains(event.target) &&
|
||||
isAddHostHidden && // Only close if addHost form is hidden
|
||||
isEditHostHidden && // Only close if editHost form is hidden
|
||||
isErrorHidden // Only close if error is hidden
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
@@ -21,7 +44,12 @@ function Launchpad({ onClose, getHosts, connectToHost }) {
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [onClose]);
|
||||
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]);
|
||||
|
||||
const handleEditHostClick = () => {
|
||||
setIsAddHostHidden(false); // Open the form for editing
|
||||
setActiveApp('hostViewer'); // Set active app to HostViewer
|
||||
};
|
||||
|
||||
return (
|
||||
<CssVarsProvider theme={theme}>
|
||||
@@ -47,16 +75,112 @@ function Launchpad({ onClose, getHosts, connectToHost }) {
|
||||
height: "75%",
|
||||
backgroundColor: theme.palette.general.tertiary,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 10px rgba(0, 0, 0, 0.3)",
|
||||
border: `1px solid ${theme.palette.general.secondary}`,
|
||||
color: theme.palette.text.primary,
|
||||
padding: 3,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<HostViewer getHosts={getHosts} connectToHost={connectToHost} />
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
style={{
|
||||
width: sidebarOpen ? "200px" : "60px",
|
||||
backgroundColor: theme.palette.general.disabled,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
padding: "10px 5px",
|
||||
transition: "width 0.3s ease",
|
||||
overflow: "hidden",
|
||||
borderRight: `1px solid ${theme.palette.general.secondary}`,
|
||||
borderTopLeftRadius: "8px",
|
||||
borderBottomLeftRadius: "8px",
|
||||
}}
|
||||
>
|
||||
{/* Sidebar Toggle Button */}
|
||||
<Button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.general.dark,
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
width: sidebarOpen ? "175px" : "40px",
|
||||
height: "40px",
|
||||
borderRadius: "8px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: "10px",
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
>
|
||||
{sidebarOpen ? "←" : "→"}
|
||||
</Button>
|
||||
|
||||
{/* HostViewer Button */}
|
||||
<Button
|
||||
onClick={() => setActiveApp('hostViewer')}
|
||||
sx={{
|
||||
backgroundColor: activeApp === 'hostViewer'
|
||||
? theme.palette.general.tertiary
|
||||
: theme.palette.general.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: activeApp === 'hostViewer'
|
||||
? theme.palette.general.tertiary
|
||||
: theme.palette.general.dark,
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
width: sidebarOpen ? "175px" : "40px",
|
||||
height: "40px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderRadius: "8px",
|
||||
paddingLeft: sidebarOpen ? "15px" : "0",
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
>
|
||||
{sidebarOpen ? (
|
||||
"Hosts"
|
||||
) : (
|
||||
<img
|
||||
src={HostViewerIcon}
|
||||
alt="Host Viewer"
|
||||
width={24}
|
||||
height={24}
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
{activeApp === 'hostViewer' && (
|
||||
<HostViewer
|
||||
getHosts={getHosts}
|
||||
connectToHost={connectToHost}
|
||||
setIsAddHostHidden={setIsAddHostHidden}
|
||||
deleteHost={deleteHost}
|
||||
editHost={editHost} // Pass editHost here
|
||||
createFolder={createFolder}
|
||||
moveHostToFolder={moveHostToFolder}
|
||||
onEditHostClick={handleEditHostClick} // Pass the handler to the form
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CssVarsProvider>
|
||||
@@ -65,8 +189,16 @@ function Launchpad({ onClose, getHosts, connectToHost }) {
|
||||
|
||||
Launchpad.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
connectToHost: PropTypes.func.isRequired,
|
||||
getHosts: PropTypes.func.isRequired,
|
||||
connectToHost: PropTypes.func.isRequired,
|
||||
isAddHostHidden: PropTypes.bool.isRequired,
|
||||
setIsAddHostHidden: PropTypes.func.isRequired,
|
||||
isEditHostHidden: PropTypes.bool.isRequired,
|
||||
isErrorHidden: PropTypes.bool.isRequired,
|
||||
deleteHost: PropTypes.func.isRequired,
|
||||
editHost: PropTypes.func.isRequired,
|
||||
createFolder: PropTypes.func.isRequired,
|
||||
moveHostToFolder: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Launchpad;
|
||||
72
src/User.jsx
72
src/User.jsx
@@ -133,7 +133,11 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
||||
});
|
||||
|
||||
socketRef.current.once("hostsFound", (data) => {
|
||||
resolve(data);
|
||||
if (data && Array.isArray(data)) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject("Invalid data received.");
|
||||
}
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
@@ -149,6 +153,68 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
||||
});
|
||||
};
|
||||
|
||||
const deleteHost = (hostConfig) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("deleteHost", {
|
||||
userId: currentUser.current.id,
|
||||
hostConfig: hostConfig,
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
onFailure(error);
|
||||
});
|
||||
} else {
|
||||
onFailure("No user is currently logged in.");
|
||||
}
|
||||
}
|
||||
|
||||
const editExistingHost = ({ userId, oldHostConfig, newHostConfig }) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("editHost", {
|
||||
userId: userId,
|
||||
oldHostConfig: oldHostConfig,
|
||||
newHostConfig: newHostConfig,
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
onFailure(error);
|
||||
});
|
||||
} else {
|
||||
onFailure("No user is currently logged in.");
|
||||
}
|
||||
};
|
||||
|
||||
const createFolder = (folderName) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("createFolder", {
|
||||
userId: currentUser.current.id,
|
||||
folderName: folderName,
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
onFailure(error);
|
||||
});
|
||||
} else {
|
||||
onFailure("No user is currently logged in.");
|
||||
}
|
||||
}
|
||||
|
||||
const moveHostToFolder = (folderName, hostConfig) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("moveHostToFolder", {
|
||||
userId: currentUser.current.id,
|
||||
folderName: folderName,
|
||||
hostConfig: hostConfig,
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
onFailure(error);
|
||||
});
|
||||
} else {
|
||||
onFailure("No user is currently logged in.");
|
||||
}
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
createUser,
|
||||
loginUser,
|
||||
@@ -157,6 +223,10 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
||||
saveHost,
|
||||
getUser,
|
||||
getAllHosts,
|
||||
deleteHost,
|
||||
editExistingHost,
|
||||
createFolder,
|
||||
moveHostToFolder,
|
||||
}));
|
||||
|
||||
return <div></div>;
|
||||
|
||||
135
src/apps/HostViewer.jsx
Normal file
135
src/apps/HostViewer.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Button } from "@mui/joy";
|
||||
|
||||
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost }) {
|
||||
const [hosts, setHosts] = useState([]);
|
||||
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
|
||||
async function fetchInitialHosts() {
|
||||
try {
|
||||
const savedHosts = await getHosts();
|
||||
if (isMounted.current) {
|
||||
setHosts(savedHosts || []);
|
||||
setInitialLoadComplete(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Initial host fetch failed:", error);
|
||||
if (isMounted.current) {
|
||||
setHosts([]);
|
||||
setInitialLoadComplete(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchInitialHosts();
|
||||
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
const savedHosts = await getHosts();
|
||||
if (isMounted.current) {
|
||||
setHosts(savedHosts || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Periodic host update failed:", error);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [getHosts]);
|
||||
|
||||
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">
|
||||
<h2 className="text-lg font-bold">Hosts</h2>
|
||||
<Button
|
||||
className="text-black"
|
||||
onClick={() => setIsAddHostHidden(false)}
|
||||
sx={{
|
||||
backgroundColor: "#6e6e6e",
|
||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
||||
}}
|
||||
>
|
||||
Add Host
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-grow overflow-auto">
|
||||
{hosts.length > 0 ? (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{hosts.map((hostWrapper, index) => {
|
||||
const hostConfig = hostWrapper.hostConfig || {};
|
||||
|
||||
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" }
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
<Button
|
||||
className="text-black"
|
||||
onClick={() => {
|
||||
deleteHost(hostConfig);
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: "#6e6e6e",
|
||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
className="text-black"
|
||||
onClick={() => {
|
||||
editHost(hostConfig);
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: "#6e6e6e",
|
||||
"&:hover": { backgroundColor: "#0f0f0f" }
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-300">Hosts are either loading or do not exist...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
HostViewer.propTypes = {
|
||||
getHosts: PropTypes.func.isRequired,
|
||||
connectToHost: PropTypes.func.isRequired,
|
||||
setIsAddHostHidden: PropTypes.func.isRequired,
|
||||
deleteHost: PropTypes.func.isRequired,
|
||||
editHost: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default HostViewer;
|
||||
@@ -141,6 +141,93 @@ async function getHosts(userId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHost(userId, hostConfig) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
user.sshConnections = user.sshConnections.filter(connection => {
|
||||
const matches =
|
||||
connection.name === hostConfig.name &&
|
||||
connection.ip === hostConfig.ip &&
|
||||
connection.port === hostConfig.port &&
|
||||
connection.user === hostConfig.user;
|
||||
|
||||
return !matches;
|
||||
});
|
||||
|
||||
await user.save();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error deleting host: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function editHost(userId, oldHostConfig, newHostConfig) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
user.sshConnections = user.sshConnections.map(connection => {
|
||||
const matches =
|
||||
connection.hostConfig.name === oldHostConfig.name &&
|
||||
connection.hostConfig.ip === oldHostConfig.ip &&
|
||||
connection.hostConfig.port === oldHostConfig.port &&
|
||||
connection.hostConfig.user === oldHostConfig.user;
|
||||
|
||||
if (matches) {
|
||||
return { hostConfig: newHostConfig };
|
||||
} else {
|
||||
return connection;
|
||||
}
|
||||
});
|
||||
|
||||
await user.save();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error editing host: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function createFolder(userId, folderName) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
user.sshConnections.push({ folderName, connections: [] });
|
||||
await user.save();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error creating folder: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function moveHostToFolder(userId, hostConfig, folderName) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
const folder = user.sshConnections.find(folder => folder.folderName === folderName);
|
||||
if (folder) {
|
||||
folder.connections.push(hostConfig);
|
||||
await user.save();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { error: 'Folder not found' };
|
||||
}
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error moving host to folder: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
dbNamespace.on("connection", (socket) => {
|
||||
console.log("New socket connection established on");
|
||||
|
||||
@@ -202,6 +289,50 @@ dbNamespace.on("connection", (socket) => {
|
||||
socket.emit(result.error ? "error" : "hostsFound", result);
|
||||
console.log(result.error || `Hosts found`);
|
||||
});
|
||||
|
||||
socket.on("deleteHost", async (data) => {
|
||||
const { userId, hostConfig } = data;
|
||||
if (!userId || !hostConfig) {
|
||||
socket.emit("error", "User ID and host config are required");
|
||||
return;
|
||||
}
|
||||
const result = await deleteHost(userId, hostConfig);
|
||||
socket.emit(result.error ? "error" : "hostDeleted", result);
|
||||
console.log(result.error || `Host deleted`);
|
||||
});
|
||||
|
||||
socket.on("editHost", async (data) => {
|
||||
const { userId, oldHostConfig, newHostConfig } = data;
|
||||
if (!userId || !oldHostConfig || !newHostConfig) {
|
||||
socket.emit("error", "User ID, old host config, and new host config are required");
|
||||
return;
|
||||
}
|
||||
const result = await editHost(userId, oldHostConfig, newHostConfig);
|
||||
socket.emit(result.error ? "error" : "hostEdited", result);
|
||||
console.log(result.error || `Host edited`);
|
||||
});
|
||||
|
||||
socket.on("createFolder", async (data) => {
|
||||
const { userId, folderName } = data;
|
||||
if (!userId || !folderName) {
|
||||
socket.emit("error", "User ID and folder name are required");
|
||||
return;
|
||||
}
|
||||
const result = await createFolder(userId, folderName);
|
||||
socket.emit(result.error ? "error" : "folderCreated", result);
|
||||
console.log(result.error || `Folder created`);
|
||||
});
|
||||
|
||||
socket.on("moveHostToFolder", async (data) => {
|
||||
const { userId, hostConfig, folderName } = data;
|
||||
if (!userId || !hostConfig || !folderName) {
|
||||
socket.emit("error", "User ID, host config, and folder name are required");
|
||||
return;
|
||||
}
|
||||
const result = await moveHostToFolder(userId, hostConfig, folderName);
|
||||
socket.emit(result.error ? "error" : "hostMoved", result);
|
||||
console.log(result.error || `Host moved to folder`);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(8082, '0.0.0.0', async () => {
|
||||
|
||||
BIN
src/images/host_viewer_icon.png
Normal file
BIN
src/images/host_viewer_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -14,7 +14,7 @@ import {
|
||||
Option,
|
||||
Checkbox
|
||||
} from '@mui/joy';
|
||||
import theme from './theme';
|
||||
import theme from '/src/theme';
|
||||
|
||||
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
|
||||
const handleFileChange = (e) => {
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { CssVarsProvider } from '@mui/joy/styles';
|
||||
import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog } from '@mui/joy';
|
||||
import theme from './theme';
|
||||
import theme from '/src/theme';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const CreateUserModal = ({ isHidden, form, setForm, handleCreateUser, setIsCreateUserHidden, setIsLoginUserHidden }) => {
|
||||
231
src/modals/EditHostModal.jsx
Normal file
231
src/modals/EditHostModal.jsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect } from 'react';
|
||||
import { CssVarsProvider } from '@mui/joy/styles';
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Stack,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
ModalDialog,
|
||||
Select,
|
||||
Option,
|
||||
Checkbox
|
||||
} from '@mui/joy';
|
||||
import theme from '/src/theme';
|
||||
|
||||
const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostHidden, hostConfig }) => {
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
if (file.name.endsWith('.rsa') || file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.der') || file.name.endsWith('.p8') || file.name.endsWith('.ssh')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
setForm({ ...form, rsaKey: event.target.result });
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
alert("Please upload a valid RSA private key file.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isFormValid = () => {
|
||||
if (form.authMethod === 'Select Auth') return false;
|
||||
if (!form.ip || !form.user || !form.port) return false;
|
||||
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
|
||||
if (form.authMethod === 'password' && !form.password) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hostConfig) {
|
||||
setForm({
|
||||
name: hostConfig.name || '',
|
||||
ip: hostConfig.ip || '',
|
||||
user: hostConfig.user || '',
|
||||
password: hostConfig.password || '',
|
||||
rsaKey: hostConfig.rsaKey || '',
|
||||
port: Number(hostConfig.port) || 22,
|
||||
authMethod: hostConfig.password ? 'password' : 'rsaKey',
|
||||
rememberHost: hostConfig.rememberHost || false,
|
||||
});
|
||||
}
|
||||
}, [hostConfig, setForm]);
|
||||
|
||||
return (
|
||||
<CssVarsProvider theme={theme}>
|
||||
<Modal open={!isHidden} onClose={() => setIsEditHostHidden(true)}>
|
||||
<ModalDialog
|
||||
layout="center"
|
||||
sx={{
|
||||
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",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Edit Host</DialogTitle>
|
||||
<DialogContent>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (isFormValid()) handleEditHost();
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
|
||||
<FormControl>
|
||||
<FormLabel>Host Name</FormLabel>
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.primary,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl error={!form.ip}>
|
||||
<FormLabel>Host IP</FormLabel>
|
||||
<Input
|
||||
value={form.ip}
|
||||
onChange={(e) => setForm({ ...form, ip: e.target.value })}
|
||||
required
|
||||
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({ ...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': {
|
||||
backgroundColor: theme.palette.general.disabled,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Option value="Select Auth" disabled>
|
||||
Select Auth
|
||||
</Option>
|
||||
<Option value="password">Password</Option>
|
||||
<Option value="rsaKey">RSA Key</Option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{form.authMethod === 'password' && (
|
||||
<FormControl error={!form.password}>
|
||||
<FormLabel>Host Password</FormLabel>
|
||||
<Input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
required
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.primary,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
{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%',
|
||||
minWidth: 'auto',
|
||||
minHeight: 'auto',
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isFormValid()}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.general.disabled,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Edit Host
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
</CssVarsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
EditHostModal.propTypes = {
|
||||
isHidden: PropTypes.bool.isRequired,
|
||||
form: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
ip: PropTypes.string.isRequired,
|
||||
user: PropTypes.string.isRequired,
|
||||
password: PropTypes.string,
|
||||
rsaKey: PropTypes.string,
|
||||
port: PropTypes.number.isRequired,
|
||||
authMethod: PropTypes.string.isRequired,
|
||||
rememberHost: PropTypes.bool,
|
||||
}).isRequired,
|
||||
setForm: PropTypes.func.isRequired,
|
||||
handleEditHost: PropTypes.func.isRequired,
|
||||
setIsEditHostHidden: PropTypes.func.isRequired,
|
||||
hostConfig: PropTypes.object,
|
||||
};
|
||||
|
||||
export default EditHostModal;
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { CssVarsProvider } from '@mui/joy/styles';
|
||||
import { Modal, Button, DialogTitle, DialogContent, ModalDialog } from '@mui/joy';
|
||||
import theme from './theme';
|
||||
import theme from '/src/theme';
|
||||
|
||||
const ErrorModal = ({ isHidden, errorMessage, setIsErrorHidden }) => {
|
||||
return (
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { CssVarsProvider } from '@mui/joy/styles';
|
||||
import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog } from '@mui/joy';
|
||||
import theme from './theme';
|
||||
import theme from '/src/theme';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
const LoginUserModal = ({ isHidden, form, setForm, handleLoginUser, setIsLoginUserHidden, setIsCreateUserHidden }) => {
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { CssVarsProvider } from '@mui/joy/styles';
|
||||
import {Modal, Button, DialogTitle, DialogContent, ModalDialog, Stack } from '@mui/joy';
|
||||
import theme from './theme';
|
||||
import theme from '/src/theme';
|
||||
|
||||
const ProfileModal = ({ isHidden, getUser, handleDeleteUser, handleLogoutUser, setIsProfileHidden }) => {
|
||||
const handleDelete = () => {
|
||||
Reference in New Issue
Block a user