Save hosts to tabs (very early version, not that many issues not just not very feature rich and has a poor UI that will be improved with more features)
This commit is contained in:
@@ -1,6 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { CssVarsProvider } from '@mui/joy/styles';
|
||||
import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog, Select, Option } from '@mui/joy';
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Stack,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
ModalDialog,
|
||||
Select,
|
||||
Option,
|
||||
Checkbox
|
||||
} from '@mui/joy';
|
||||
import theme from './theme';
|
||||
|
||||
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
|
||||
@@ -160,6 +173,16 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Remember Host</FormLabel>
|
||||
<Checkbox
|
||||
checked={form.rememberHost ?? false}
|
||||
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isFormValid()}
|
||||
@@ -191,6 +214,7 @@ AddHostModal.propTypes = {
|
||||
rsaKey: PropTypes.string,
|
||||
port: PropTypes.number.isRequired,
|
||||
authMethod: PropTypes.string.isRequired,
|
||||
rememberHost: PropTypes.bool,
|
||||
}).isRequired,
|
||||
setForm: PropTypes.func.isRequired,
|
||||
handleAddHost: PropTypes.func.isRequired,
|
||||
|
||||
87
src/App.jsx
87
src/App.jsx
@@ -34,6 +34,7 @@ function App() {
|
||||
password: "",
|
||||
port: 22,
|
||||
authMethod: "Select Auth",
|
||||
rememberHost: false,
|
||||
});
|
||||
const [loginUserForm, setLoginUserForm] = useState({
|
||||
username: "",
|
||||
@@ -123,29 +124,65 @@ function App() {
|
||||
}, []);
|
||||
|
||||
const handleAddHost = () => {
|
||||
if (addHostForm.ip && addHostForm.user && ((addHostForm.authMethod === 'password' && addHostForm.password) || (addHostForm.authMethod === 'rsaKey' && addHostForm.rsaKey)) && addHostForm.port) {
|
||||
const newTerminal = {
|
||||
id: nextId,
|
||||
title: addHostForm.name || addHostForm.ip,
|
||||
hostConfig: {
|
||||
ip: addHostForm.ip,
|
||||
user: addHostForm.user,
|
||||
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
||||
rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
|
||||
port: String(addHostForm.port),
|
||||
},
|
||||
terminalRef: null,
|
||||
};
|
||||
setTerminals([...terminals, newTerminal]);
|
||||
setActiveTab(nextId);
|
||||
setNextId(nextId + 1);
|
||||
setIsAddHostHidden(true);
|
||||
setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" });
|
||||
if (addHostForm.ip && addHostForm.user && ((addHostForm.authMethod === 'password' && addHostForm.password) || (addHostForm.authMethod === 'rsaKey' && addHostForm.rsaKey)) && addHostForm.port && addHostForm.authMethod !== 'Select Auth') {
|
||||
connectToHost();
|
||||
if (addHostForm.rememberHost) {
|
||||
handleSaveHost();
|
||||
}
|
||||
} else {
|
||||
alert("Please fill out all fields.");
|
||||
}
|
||||
};
|
||||
|
||||
const connectToHost = () => {
|
||||
const newTerminal = {
|
||||
id: nextId,
|
||||
title: addHostForm.name || addHostForm.ip,
|
||||
hostConfig: {
|
||||
ip: addHostForm.ip,
|
||||
user: addHostForm.user,
|
||||
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
||||
rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
|
||||
port: String(addHostForm.port),
|
||||
},
|
||||
terminalRef: null,
|
||||
};
|
||||
setTerminals([...terminals, newTerminal]);
|
||||
setActiveTab(nextId);
|
||||
setNextId(nextId + 1);
|
||||
setIsAddHostHidden(true);
|
||||
setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" });
|
||||
}
|
||||
|
||||
const connectToHostWithConfig = (hostConfig) => {
|
||||
const newTerminal = {
|
||||
id: nextId,
|
||||
title: hostConfig.name || hostConfig.ip,
|
||||
hostConfig: hostConfig,
|
||||
terminalRef: null,
|
||||
};
|
||||
setTerminals([...terminals, newTerminal]);
|
||||
setActiveTab(nextId);
|
||||
setNextId(nextId + 1);
|
||||
setIsLaunchpadOpen(false);
|
||||
}
|
||||
|
||||
const handleSaveHost = () => {
|
||||
let hostConfig = {
|
||||
name: addHostForm.name,
|
||||
ip: addHostForm.ip,
|
||||
user: addHostForm.user,
|
||||
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
|
||||
rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
|
||||
port: String(addHostForm.port),
|
||||
}
|
||||
if (userRef.current) {
|
||||
userRef.current.saveHost({
|
||||
hostConfig,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => {
|
||||
if (userRef.current) {
|
||||
if (sessionToken) {
|
||||
@@ -198,6 +235,12 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const getHosts = () => {
|
||||
if (userRef.current) {
|
||||
return userRef.current.getAllHosts();
|
||||
}
|
||||
}
|
||||
|
||||
const closeTab = (id) => {
|
||||
const newTerminals = terminals.filter((t) => t.id !== id);
|
||||
setTerminals(newTerminals);
|
||||
@@ -369,7 +412,13 @@ function App() {
|
||||
errorMessage={errorMessage}
|
||||
setIsErrorHidden={setIsErrorHidden}
|
||||
/>
|
||||
{isLaunchpadOpen && <Launchpad onClose={() => setIsLaunchpadOpen(false)} />}
|
||||
{isLaunchpadOpen && (
|
||||
<Launchpad
|
||||
onClose={() => setIsLaunchpadOpen(false)}
|
||||
getHosts={getHosts}
|
||||
connectToHost={connectToHostWithConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
<LoginUserModal
|
||||
isHidden={isLoginUserHidden}
|
||||
|
||||
115
src/Apps/HostViewer.jsx
Normal file
115
src/Apps/HostViewer.jsx
Normal file
@@ -0,0 +1,115 @@
|
||||
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,10 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { CssVarsProvider } from '@mui/joy/styles';
|
||||
import { Button } from '@mui/joy';
|
||||
import theme from './theme';
|
||||
|
||||
function Launchpad({ onClose }) {
|
||||
// Apps
|
||||
import HostViewer from './Apps/HostViewer';
|
||||
|
||||
function Launchpad({ onClose, getHosts, connectToHost }) {
|
||||
const launchpadRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -54,25 +56,7 @@ function Launchpad({ onClose }) {
|
||||
padding: 3,
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold mb-4">Launchpad</h2>
|
||||
<p className="mb-4">A one-stop shop for adding hosts, apps (AI, notes, etc.), and all new features to come! Coming to you in a future update. Stay tuned!</p>
|
||||
<p className="mb-4">
|
||||
Can also be opened using <code className="bg-gray-500 px-1 rounded">Ctrl + L</code>
|
||||
</p>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.general.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.general.disabled,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
<HostViewer getHosts={getHosts} connectToHost={connectToHost} />
|
||||
</div>
|
||||
</div>
|
||||
</CssVarsProvider>
|
||||
@@ -81,6 +65,8 @@ function Launchpad({ onClose }) {
|
||||
|
||||
Launchpad.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
connectToHost: PropTypes.func.isRequired,
|
||||
getHosts: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Launchpad;
|
||||
41
src/User.jsx
41
src/User.jsx
@@ -106,16 +106,57 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
|
||||
}
|
||||
};
|
||||
|
||||
const saveHost = (hostConfig) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("saveHostConfig", {
|
||||
userId: currentUser.current.id,
|
||||
hostConfig: hostConfig,
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
onFailure(error);
|
||||
});
|
||||
} else {
|
||||
onFailure("No user is currently logged in.");
|
||||
}
|
||||
}
|
||||
|
||||
const getUser = () => {
|
||||
return currentUser.current;
|
||||
}
|
||||
|
||||
const getAllHosts = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (currentUser.current?.id && socketRef.current) {
|
||||
socketRef.current.emit("getHosts", {
|
||||
userId: currentUser.current.id,
|
||||
});
|
||||
|
||||
socketRef.current.once("hostsFound", (data) => {
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
socketRef.current.once("error", (error) => {
|
||||
console.error(error);
|
||||
const errorMsg = (error && typeof error === 'object' && error !== null)
|
||||
? error.error || error.message || 'An error occurred'
|
||||
: String(error);
|
||||
reject(errorMsg);
|
||||
});
|
||||
} else {
|
||||
reject("No user is currently logged in.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
createUser,
|
||||
loginUser,
|
||||
logoutUser,
|
||||
deleteUser,
|
||||
saveHost,
|
||||
getUser,
|
||||
getAllHosts,
|
||||
}));
|
||||
|
||||
return <div></div>;
|
||||
|
||||
@@ -113,6 +113,34 @@ async function deleteUser(userId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHostConfig(userId, hostConfig) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
user.sshConnections.push(hostConfig);
|
||||
await user.save();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error saving host config: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function getHosts(userId) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
return user.sshConnections;
|
||||
} else {
|
||||
return { error: 'User not found' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: 'Error getting hosts: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
dbNamespace.on("connection", (socket) => {
|
||||
console.log("New socket connection established on");
|
||||
|
||||
@@ -152,6 +180,28 @@ dbNamespace.on("connection", (socket) => {
|
||||
socket.emit(result.error ? "error" : "userDeleted", result);
|
||||
console.log(result.error || `User deleted`);
|
||||
});
|
||||
|
||||
socket.on("saveHostConfig", async (data) => {
|
||||
const { userId, hostConfig } = data;
|
||||
if (!userId || !hostConfig) {
|
||||
socket.emit("error", "User ID and host config are required");
|
||||
return;
|
||||
}
|
||||
const result = await saveHostConfig(userId, hostConfig);
|
||||
socket.emit(result.error ? "error" : "hostConfigSaved", result);
|
||||
console.log(result.error || `Host config saved`);
|
||||
});
|
||||
|
||||
socket.on("getHosts", async (data) => {
|
||||
const { userId } = data;
|
||||
if (!userId) {
|
||||
socket.emit("error", "User ID is required");
|
||||
return;
|
||||
}
|
||||
const result = await getHosts(userId);
|
||||
socket.emit(result.error ? "error" : "hostsFound", result);
|
||||
console.log(result.error || `Hosts found`);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(8082, '0.0.0.0', async () => {
|
||||
|
||||
Reference in New Issue
Block a user