Updated launchpad UI to be expandable in the future. Updated UI for the hosts to be able to easily configure them. They stil need organizational system (folders, etc.)
This commit is contained in:
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