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:
Karmaa
2025-03-13 22:42:18 -05:00
parent e41bec5e4d
commit e2e35e6130
13 changed files with 802 additions and 136 deletions

View File

@@ -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}
/>
)}

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

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

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

View File

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

View File

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

View File

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