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 PropTypes from 'prop-types';
|
||||||
import { CssVarsProvider } from '@mui/joy/styles';
|
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';
|
import theme from './theme';
|
||||||
|
|
||||||
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
|
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
|
||||||
@@ -160,6 +173,16 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</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
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isFormValid()}
|
disabled={!isFormValid()}
|
||||||
@@ -191,6 +214,7 @@ AddHostModal.propTypes = {
|
|||||||
rsaKey: PropTypes.string,
|
rsaKey: PropTypes.string,
|
||||||
port: PropTypes.number.isRequired,
|
port: PropTypes.number.isRequired,
|
||||||
authMethod: PropTypes.string.isRequired,
|
authMethod: PropTypes.string.isRequired,
|
||||||
|
rememberHost: PropTypes.bool,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
setForm: PropTypes.func.isRequired,
|
setForm: PropTypes.func.isRequired,
|
||||||
handleAddHost: PropTypes.func.isRequired,
|
handleAddHost: PropTypes.func.isRequired,
|
||||||
|
|||||||
57
src/App.jsx
57
src/App.jsx
@@ -34,6 +34,7 @@ function App() {
|
|||||||
password: "",
|
password: "",
|
||||||
port: 22,
|
port: 22,
|
||||||
authMethod: "Select Auth",
|
authMethod: "Select Auth",
|
||||||
|
rememberHost: false,
|
||||||
});
|
});
|
||||||
const [loginUserForm, setLoginUserForm] = useState({
|
const [loginUserForm, setLoginUserForm] = useState({
|
||||||
username: "",
|
username: "",
|
||||||
@@ -123,7 +124,17 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAddHost = () => {
|
const handleAddHost = () => {
|
||||||
if (addHostForm.ip && addHostForm.user && ((addHostForm.authMethod === 'password' && addHostForm.password) || (addHostForm.authMethod === 'rsaKey' && addHostForm.rsaKey)) && addHostForm.port) {
|
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 = {
|
const newTerminal = {
|
||||||
id: nextId,
|
id: nextId,
|
||||||
title: addHostForm.name || addHostForm.ip,
|
title: addHostForm.name || addHostForm.ip,
|
||||||
@@ -141,10 +152,36 @@ function App() {
|
|||||||
setNextId(nextId + 1);
|
setNextId(nextId + 1);
|
||||||
setIsAddHostHidden(true);
|
setIsAddHostHidden(true);
|
||||||
setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" });
|
setAddHostForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth" });
|
||||||
} else {
|
|
||||||
alert("Please fill out all fields.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 }) => {
|
const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => {
|
||||||
if (userRef.current) {
|
if (userRef.current) {
|
||||||
@@ -198,6 +235,12 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getHosts = () => {
|
||||||
|
if (userRef.current) {
|
||||||
|
return userRef.current.getAllHosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const closeTab = (id) => {
|
const closeTab = (id) => {
|
||||||
const newTerminals = terminals.filter((t) => t.id !== id);
|
const newTerminals = terminals.filter((t) => t.id !== id);
|
||||||
setTerminals(newTerminals);
|
setTerminals(newTerminals);
|
||||||
@@ -369,7 +412,13 @@ function App() {
|
|||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
setIsErrorHidden={setIsErrorHidden}
|
setIsErrorHidden={setIsErrorHidden}
|
||||||
/>
|
/>
|
||||||
{isLaunchpadOpen && <Launchpad onClose={() => setIsLaunchpadOpen(false)} />}
|
{isLaunchpadOpen && (
|
||||||
|
<Launchpad
|
||||||
|
onClose={() => setIsLaunchpadOpen(false)}
|
||||||
|
getHosts={getHosts}
|
||||||
|
connectToHost={connectToHostWithConfig}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<LoginUserModal
|
<LoginUserModal
|
||||||
isHidden={isLoginUserHidden}
|
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 PropTypes from 'prop-types';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { CssVarsProvider } from '@mui/joy/styles';
|
import { CssVarsProvider } from '@mui/joy/styles';
|
||||||
import { Button } from '@mui/joy';
|
|
||||||
import theme from './theme';
|
import theme from './theme';
|
||||||
|
|
||||||
function Launchpad({ onClose }) {
|
// Apps
|
||||||
|
import HostViewer from './Apps/HostViewer';
|
||||||
|
|
||||||
|
function Launchpad({ onClose, getHosts, connectToHost }) {
|
||||||
const launchpadRef = useRef(null);
|
const launchpadRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -54,25 +56,7 @@ function Launchpad({ onClose }) {
|
|||||||
padding: 3,
|
padding: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-center">
|
<HostViewer getHosts={getHosts} connectToHost={connectToHost} />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CssVarsProvider>
|
</CssVarsProvider>
|
||||||
@@ -81,6 +65,8 @@ function Launchpad({ onClose }) {
|
|||||||
|
|
||||||
Launchpad.propTypes = {
|
Launchpad.propTypes = {
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
connectToHost: PropTypes.func.isRequired,
|
||||||
|
getHosts: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Launchpad;
|
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 = () => {
|
const getUser = () => {
|
||||||
return currentUser.current;
|
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, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
createUser,
|
createUser,
|
||||||
loginUser,
|
loginUser,
|
||||||
logoutUser,
|
logoutUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
saveHost,
|
||||||
getUser,
|
getUser,
|
||||||
|
getAllHosts,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return <div></div>;
|
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) => {
|
dbNamespace.on("connection", (socket) => {
|
||||||
console.log("New socket connection established on");
|
console.log("New socket connection established on");
|
||||||
|
|
||||||
@@ -152,6 +180,28 @@ dbNamespace.on("connection", (socket) => {
|
|||||||
socket.emit(result.error ? "error" : "userDeleted", result);
|
socket.emit(result.error ? "error" : "userDeleted", result);
|
||||||
console.log(result.error || `User deleted`);
|
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 () => {
|
server.listen(8082, '0.0.0.0', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user