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:
Karmaa
2025-03-12 23:30:34 -05:00
parent 994de92451
commit e41bec5e4d
6 changed files with 306 additions and 41 deletions

View File

@@ -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,

View File

@@ -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
View 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;

View File

@@ -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;

View File

@@ -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>;

View File

@@ -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 () => {