Improved and fixed editing and adding host UI

This commit is contained in:
Karmaa
2025-03-17 23:38:43 -05:00
parent d71352045c
commit 0a50d5c85c
4 changed files with 597 additions and 623 deletions

View File

@@ -222,13 +222,12 @@ function App() {
}, []); }, []);
const handleAddHost = () => { const handleAddHost = () => {
if (addHostForm.ip && addHostForm.user && addHostForm.port) { if (!addHostForm.ip?.trim() || !addHostForm.user?.trim() || !addHostForm.port) {
if (!addHostForm.rememberHost) { alert("Please fill out all required fields (IP, User, Port).");
connectToHost(); return;
setIsAddHostHidden(true); }
return;
}
if (addHostForm.rememberHost) {
if (addHostForm.authMethod === 'Select Auth') { if (addHostForm.authMethod === 'Select Auth') {
alert("Please select an authentication method."); alert("Please select an authentication method.");
return; return;
@@ -237,33 +236,42 @@ function App() {
setIsNoAuthHidden(false); setIsNoAuthHidden(false);
return; return;
} }
if (addHostForm.authMethod === 'rsaKey' && !addHostForm.rsaKey) { if (addHostForm.authMethod === 'key' && !addHostForm.privateKey) {
setIsNoAuthHidden(false); setIsNoAuthHidden(false);
return; return;
} }
}
connectToHost(); connectToHost();
if (addHostForm.rememberHost) {
if (!addHostForm.storePassword) { if (!addHostForm.storePassword) {
addHostForm.password = ''; addHostForm.password = '';
addHostForm.privateKey = '';
} }
handleSaveHost(); handleSaveHost();
setIsAddHostHidden(true);
} else {
alert("Please fill out all required fields (IP, User, Port).");
} }
setIsAddHostHidden(true);
}; };
const connectToHost = () => { const connectToHost = () => {
const hostConfig = { const hostConfig = {
name: addHostForm.name || '', name: addHostForm.name || '',
folder: addHostForm.folder || '', folder: addHostForm.folder || '',
ip: addHostForm.ip, ip: addHostForm.ip.trim(),
user: addHostForm.user, user: addHostForm.user.trim(),
port: String(addHostForm.port), port: String(addHostForm.port),
password: addHostForm.rememberHost && addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
rsaKey: addHostForm.rememberHost && addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
}; };
if (addHostForm.rememberHost && addHostForm.storePassword) {
if (addHostForm.authMethod === 'password') {
hostConfig.password = addHostForm.password;
} else if (addHostForm.authMethod === 'key') {
hostConfig.privateKey = addHostForm.privateKey;
hostConfig.keyType = addHostForm.keyType;
hostConfig.passphrase = addHostForm.passphrase;
}
}
const newTerminal = { const newTerminal = {
id: nextId, id: nextId,
title: hostConfig.name || hostConfig.ip, title: hostConfig.name || hostConfig.ip,
@@ -273,9 +281,21 @@ function App() {
setTerminals([...terminals, newTerminal]); setTerminals([...terminals, newTerminal]);
setActiveTab(nextId); setActiveTab(nextId);
setNextId(nextId + 1); setNextId(nextId + 1);
setIsAddHostHidden(true); setAddHostForm({
setAddHostForm({ name: "", folder: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "Select Auth", rememberHost: false, storePassword: true }); name: "",
} folder: "",
ip: "",
user: "",
password: "",
privateKey: "",
keyType: "",
passphrase: "",
port: 22,
authMethod: "Select Auth",
rememberHost: true,
storePassword: true
});
};
const handleAuthSubmit = (form) => { const handleAuthSubmit = (form) => {
const updatedTerminals = terminals.map((terminal) => { const updatedTerminals = terminals.map((terminal) => {
@@ -327,21 +347,30 @@ function App() {
} }
const handleSaveHost = () => { const handleSaveHost = () => {
let hostConfig = { const hostConfig = {
name: addHostForm.name || addHostForm.ip, name: addHostForm.name || addHostForm.ip,
folder: addHostForm.folder, folder: addHostForm.folder,
ip: addHostForm.ip, ip: addHostForm.ip.trim(),
user: addHostForm.user, user: addHostForm.user.trim(),
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
rsaKey: addHostForm.authMethod === 'rsaKey' ? addHostForm.rsaKey : undefined,
port: String(addHostForm.port), port: String(addHostForm.port),
};
if (addHostForm.storePassword) {
if (addHostForm.authMethod === 'password') {
hostConfig.password = addHostForm.password;
} else if (addHostForm.authMethod === 'key') {
hostConfig.privateKey = addHostForm.privateKey;
hostConfig.keyType = addHostForm.keyType;
hostConfig.passphrase = addHostForm.passphrase;
}
} }
if (userRef.current) { if (userRef.current) {
userRef.current.saveHost({ userRef.current.saveHost({
hostConfig, hostConfig,
}); });
} }
} };
const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => { const handleLoginUser = ({ username, password, sessionToken, onSuccess, onFailure }) => {
if (userRef.current) { if (userRef.current) {
@@ -472,7 +501,6 @@ function App() {
updateEditHostForm(oldConfig); updateEditHostForm(oldConfig);
} catch (error) { } catch (error) {
console.error('Edit failed:', error);
setErrorMessage(`Edit failed: ${error}`); setErrorMessage(`Edit failed: ${error}`);
setIsErrorHidden(false); setIsErrorHidden(false);
setIsEditing(false); setIsEditing(false);
@@ -658,10 +686,10 @@ function App() {
)} )}
<NoAuthenticationModal <NoAuthenticationModal
isHidden={isNoAuthHidden} isHidden={isNoAuthHidden}
setIsHidden={setIsNoAuthHidden}
form={authForm} form={authForm}
setForm={setAuthForm} setForm={setAuthForm}
setIsNoAuthHidden={setIsNoAuthHidden} onAuthenticate={handleAuthSubmit}
handleAuthSubmit={handleAuthSubmit}
/> />
</div> </div>
@@ -681,7 +709,7 @@ function App() {
setForm={setEditHostForm} setForm={setEditHostForm}
handleEditHost={handleEditHost} handleEditHost={handleEditHost}
setIsEditHostHidden={setIsEditHostHidden} setIsEditHostHidden={setIsEditHostHidden}
hostConfig={currentHostConfig} hostConfig={currentHostConfig || {}}
/> />
<ProfileModal <ProfileModal
isHidden={isProfileHidden} isHidden={isProfileHidden}

View File

@@ -222,7 +222,6 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
if (!currentUser.current) return onFailure("Not authenticated"); if (!currentUser.current) return onFailure("Not authenticated");
try { try {
console.log('Editing host with configs:', { oldHostConfig, newHostConfig });
const response = await new Promise((resolve) => { const response = await new Promise((resolve) => {
socketRef.current.emit("editHost", { socketRef.current.emit("editHost", {
userId: currentUser.current.id, userId: currentUser.current.id,

View File

@@ -24,20 +24,7 @@ import { useState } from 'react';
import Visibility from '@mui/icons-material/Visibility'; import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff'; import VisibilityOff from '@mui/icons-material/VisibilityOff';
const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => { const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
const [form, setForm] = useState({
name: '',
folder: '',
ip: '',
user: '',
port: 22,
password: '',
privateKey: '',
keyType: '',
passphrase: '',
authMethod: 'Select Auth',
rememberHost: true
});
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showPassphrase, setShowPassphrase] = useState(false); const [showPassphrase, setShowPassphrase] = useState(false);
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
@@ -100,14 +87,23 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => {
}; };
const isFormValid = () => { const isFormValid = () => {
if (!form.ip || !form.user || !form.port) return false; const { ip, user, port, authMethod, password, privateKey } = form;
const portNum = Number(form.port);
// Basic validation for required fields
if (!ip?.trim() || !user?.trim() || !port) return false;
// Port validation
const portNum = Number(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false; if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
// If not remembering host, only basic fields are required
if (!form.rememberHost) return true;
// Auth method validation only if remembering host
if (form.rememberHost) { if (form.rememberHost) {
if (form.authMethod === 'Select Auth') return false; if (authMethod === 'Select Auth') return false;
if (form.authMethod === 'key' && !form.privateKey) return false; if (authMethod === 'password' && !password?.trim()) return false;
if (form.authMethod === 'password' && !form.password) return false; if (authMethod === 'key' && !privateKey?.trim()) return false;
} }
return true; return true;
@@ -115,28 +111,11 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => {
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
if (isFormValid()) { if (!form.ip?.trim() || !form.user?.trim() || !form.port) {
if (!form.rememberHost) { alert("Please fill out all required fields (IP, User, Port).");
handleAddHost(); return;
} else {
handleAddHost();
}
setForm({
name: '',
folder: '',
ip: '',
user: '',
password: '',
privateKey: '',
keyType: '',
port: 22,
authMethod: 'Select Auth',
rememberHost: false,
storePassword: true,
});
setIsAddHostHidden(true);
} }
handleAddHost();
}; };
return ( return (
@@ -146,15 +125,18 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => {
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backdropFilter: 'blur(5px)',
backgroundColor: 'rgba(0, 0, 0, 0.2)',
}} }}
> >
<ModalDialog <ModalDialog
layout="center" layout="center"
variant="outlined"
sx={{ sx={{
backgroundColor: theme.palette.general.tertiary, backgroundColor: theme.palette.general.tertiary,
borderColor: theme.palette.general.secondary, borderColor: theme.palette.general.secondary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: 3, padding: 0,
borderRadius: 10, borderRadius: 10,
maxWidth: '500px', maxWidth: '500px',
width: '100%', width: '100%',
@@ -164,137 +146,239 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => {
mx: 2, mx: 2,
}} }}
> >
<DialogTitle sx={{ mb: 2 }}>Add Host</DialogTitle> <Tabs
<DialogContent> value={activeTab}
<form onSubmit={handleSubmit}> onChange={(e, val) => setActiveTab(val)}
<Tabs sx={{
value={activeTab} width: '100%',
onChange={(e, val) => setActiveTab(val)} mb: 0,
sx={{ backgroundColor: theme.palette.general.tertiary,
backgroundColor: theme.palette.general.disabled, }}
borderRadius: '8px', >
padding: '8px', <TabList
marginBottom: '16px', sx={{
width: '100%', width: '100%',
}} gap: 0,
> borderTopLeftRadius: 10,
<TabList borderTopRightRadius: 10,
sx={{ backgroundColor: theme.palette.general.primary,
width: '100%', '& button': {
gap: 0, flex: 1,
mb: 2, bgcolor: 'transparent',
'& button': { color: theme.palette.text.secondary,
flex: 1, '&:hover': {
bgcolor: 'transparent', bgcolor: theme.palette.general.disabled,
color: theme.palette.text.secondary, },
'&:hover': { '&.Mui-selected': {
bgcolor: 'rgba(255, 255, 255, 0.1)', bgcolor: theme.palette.general.tertiary,
}, color: theme.palette.text.primary,
'&.Mui-selected': { '&:hover': {
bgcolor: theme.palette.general.primary, bgcolor: theme.palette.general.tertiary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.primary,
},
},
}, },
}} },
> },
<Tab>Basic Info</Tab> }}
<Tab>Connection</Tab> >
<Tab>Authentication</Tab> <Tab sx={{ flex: 1 }}>Basic Info</Tab>
</TabList> <Tab sx={{ flex: 1 }}>Connection</Tab>
<Tab sx={{ flex: 1 }}>Authentication</Tab>
</TabList>
<TabPanel value={0}> <div style={{ padding: '24px', backgroundColor: theme.palette.general.tertiary }}>
<Stack spacing={2}> <TabPanel value={0}>
<FormControl> <Stack spacing={2}>
<FormLabel>Host Name</FormLabel> <FormControl>
<Input <FormLabel>Host Name</FormLabel>
value={form.name} <Input
onChange={(e) => setForm({ ...form, name: e.target.value })} value={form.name}
sx={{ onChange={(e) => setForm({ ...form, name: e.target.value })}
backgroundColor: theme.palette.general.primary, sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Folder</FormLabel>
<Input
value={form.folder || ''}
onChange={(e) => setForm({ ...form, folder: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
<FormControl>
<FormLabel>Remember Host</FormLabel>
<Checkbox
checked={Boolean(form.rememberHost)}
onChange={(e) => setForm({
...form,
rememberHost: e.target.checked,
})}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary, color: theme.palette.text.primary,
}} },
/> }}
</FormControl> />
<FormControl> </FormControl>
<FormLabel>Folder</FormLabel> </Stack>
<Input </TabPanel>
value={form.folder || ''}
onChange={(e) => setForm({ ...form, folder: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</FormControl>
</Stack>
</TabPanel>
<TabPanel value={1}> <TabPanel value={1}>
<Stack spacing={2}> <Stack spacing={2}>
<FormControl error={!form.ip}> <FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel> <FormLabel>Host IP</FormLabel>
<Input <Input
value={form.ip} value={form.ip}
onChange={(e) => setForm({ ...form, ip: e.target.value })} onChange={(e) => setForm({ ...form, ip: e.target.value })}
required required
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
}} }}
/> />
</FormControl> </FormControl>
<FormControl error={!form.user}> <FormControl error={!form.user}>
<FormLabel>Host User</FormLabel> <FormLabel>Host User</FormLabel>
<Input <Input
value={form.user} value={form.user}
onChange={(e) => setForm({ ...form, user: e.target.value })} onChange={(e) => setForm({ ...form, user: e.target.value })}
required required
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
}} }}
/> />
</FormControl> </FormControl>
<FormControl error={form.port < 1 || form.port > 65535}> <FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel> <FormLabel>Host Port</FormLabel>
<Input <Input
type="number" type="number"
value={form.port} value={form.port}
onChange={(e) => setForm({ ...form, port: e.target.value })} onChange={(e) => setForm({ ...form, port: e.target.value })}
min={1} min={1}
max={65535} max={65535}
required required
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
}} }}
/> />
</FormControl> </FormControl>
</Stack> </Stack>
</TabPanel> </TabPanel>
<TabPanel value={2}> <TabPanel value={2}>
<Stack spacing={2}> <Stack spacing={2}>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod}
onChange={(e, val) => handleAuthChange(val)}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
>
<Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option>
<Option value="key">SSH Key</Option>
</Select>
</FormControl>
{form.authMethod === 'password' && (
<FormControl error={!form.password}>
<FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'key' && (
<Stack spacing={2}>
<FormControl error={!form.privateKey}>
<FormLabel>SSH Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.privateKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'}
<Input
type="file"
onChange={handleFileChange}
sx={{ display: 'none' }}
/>
</Button>
</FormControl>
{form.privateKey && (
<FormControl>
<FormLabel>Key Passphrase (optional)</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassphrase ? "text" : "password"}
value={form.passphrase || ''}
onChange={(e) => setForm(prev => ({ ...prev, passphrase: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassphrase(!showPassphrase)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassphrase ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
</Stack>
)}
{form.rememberHost && (
<FormControl> <FormControl>
<FormLabel>Remember Host</FormLabel> <FormLabel>Store Password</FormLabel>
<Checkbox <Checkbox
checked={form.rememberHost} checked={Boolean(form.storePassword)}
onChange={(e) => setForm({ onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
...form,
rememberHost: e.target.checked,
...((!e.target.checked) && {
authMethod: 'Select Auth',
password: '',
privateKey: '',
keyType: '',
passphrase: '',
storePassword: true
})
})}
sx={{ sx={{
color: theme.palette.text.primary, color: theme.palette.text.primary,
'&.Mui-checked': { '&.Mui-checked': {
@@ -303,147 +387,31 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => {
}} }}
/> />
</FormControl> </FormControl>
{form.rememberHost && ( )}
<> </Stack>
<FormControl> </TabPanel>
<FormLabel>Store Password</FormLabel> </div>
<Checkbox
checked={form.storePassword}
onChange={(e) => setForm({ ...form, storePassword: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>
<FormControl error={!form.authMethod || form.authMethod === 'Select Auth'}>
<FormLabel>Authentication Method</FormLabel>
<Select
value={form.authMethod}
onChange={(e, val) => handleAuthChange(val)}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
>
<Option value="Select Auth" disabled>Select Auth</Option>
<Option value="password">Password</Option>
<Option value="key">SSH Key</Option>
</Select>
</FormControl>
{form.authMethod === 'password' && ( <Button
<FormControl error={!form.password}> onClick={handleSubmit}
<FormLabel>Password</FormLabel> sx={{
<div style={{ display: 'flex', alignItems: 'center' }}> backgroundColor: theme.palette.general.primary,
<Input color: theme.palette.text.primary,
type={showPassword ? 'text' : 'password'} '&:hover': {
value={form.password} backgroundColor: theme.palette.general.disabled,
onChange={(e) => setForm({ ...form, password: e.target.value })} },
sx={{ '&:disabled': {
backgroundColor: theme.palette.general.primary, backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: theme.palette.text.primary, color: 'rgba(255, 255, 255, 0.3)',
flex: 1 },
}} marginTop: 1,
/> width: '100%',
<IconButton height: '40px',
onClick={() => setShowPassword(!showPassword)} }}
sx={{ >
color: theme.palette.text.primary, Add Host
marginLeft: 1 </Button>
}} </Tabs>
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'key' && form.rememberHost && (
<Stack spacing={2}>
<FormControl error={!form.privateKey}>
<FormLabel>SSH Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
},
}}
>
{form.privateKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'}
<Input
type="file"
onChange={handleFileChange}
sx={{ display: 'none' }}
/>
</Button>
</FormControl>
{form.privateKey && (
<FormControl>
<FormLabel>Key Passphrase (optional)</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassphrase ? "text" : "password"}
value={form.passphrase || ''}
onChange={(e) => setForm(prev => ({ ...prev, passphrase: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassphrase(!showPassphrase)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassphrase ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
</Stack>
)}
</>
)}
</Stack>
</TabPanel>
</Tabs>
<Button
type="submit"
disabled={!isFormValid()}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
'&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)',
},
marginTop: 3,
width: '100%',
height: '40px',
}}
>
Add Host
</Button>
</form>
</DialogContent>
</ModalDialog> </ModalDialog>
</Modal> </Modal>
</CssVarsProvider> </CssVarsProvider>
@@ -452,8 +420,10 @@ const AddHostModal = ({ isHidden, setIsAddHostHidden, handleAddHost }) => {
AddHostModal.propTypes = { AddHostModal.propTypes = {
isHidden: PropTypes.bool.isRequired, isHidden: PropTypes.bool.isRequired,
setIsAddHostHidden: PropTypes.func.isRequired, form: PropTypes.object.isRequired,
setForm: PropTypes.func.isRequired,
handleAddHost: PropTypes.func.isRequired, handleAddHost: PropTypes.func.isRequired,
setIsAddHostHidden: PropTypes.func.isRequired,
}; };
export default AddHostModal; export default AddHostModal;

View File

@@ -36,7 +36,8 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
keyType: hostConfig?.keyType || '', keyType: hostConfig?.keyType || '',
passphrase: '', passphrase: '',
authMethod: hostConfig?.authMethod || 'Select Auth', authMethod: hostConfig?.authMethod || 'Select Auth',
storePassword: true storePassword: true,
rememberHost: true
}); });
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showPassphrase, setShowPassphrase] = useState(false); const [showPassphrase, setShowPassphrase] = useState(false);
@@ -51,11 +52,13 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
ip: hostConfig.ip || '', ip: hostConfig.ip || '',
user: hostConfig.user || '', user: hostConfig.user || '',
password: hostConfig.password || '', password: hostConfig.password || '',
rsaKey: hostConfig.rsaKey || '', privateKey: hostConfig.privateKey || '',
keyType: hostConfig.keyType || '',
passphrase: hostConfig.passphrase || '',
port: hostConfig.port || 22, port: hostConfig.port || 22,
authMethod: hostConfig.password ? 'password' : hostConfig.rsaKey ? 'rsaKey' : 'Select Auth', authMethod: hostConfig.password ? 'password' : hostConfig.privateKey ? 'key' : 'Select Auth',
rememberHost: true, rememberHost: true,
storePassword: !!(hostConfig.password || hostConfig.rsaKey), storePassword: !!(hostConfig.password || hostConfig.privateKey),
}); });
} }
}, [isHidden, hostConfig]); }, [isHidden, hostConfig]);
@@ -118,20 +121,28 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
...prev, ...prev,
storePassword: Boolean(checked), storePassword: Boolean(checked),
password: checked ? prev.password : "", password: checked ? prev.password : "",
rsaKey: checked ? prev.rsaKey : "", privateKey: checked ? prev.privateKey : "",
passphrase: checked ? prev.passphrase : "",
authMethod: checked ? prev.authMethod : "Select Auth" authMethod: checked ? prev.authMethod : "Select Auth"
})); }));
}; };
const isFormValid = () => { const isFormValid = () => {
const { ip, user, port, authMethod, password, rsaKey, storePassword } = form; const { ip, user, port, authMethod, password, privateKey } = form;
// Basic validation for required fields
if (!ip?.trim() || !user?.trim() || !port) return false; if (!ip?.trim() || !user?.trim() || !port) return false;
// Port validation
const portNum = Number(port); const portNum = Number(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false; if (isNaN(portNum) || portNum < 1 || portNum > 65535) return false;
if (Boolean(storePassword) && authMethod === 'password' && !password?.trim()) return false; // Auth method validation only if storing password
if (Boolean(storePassword) && authMethod === 'rsaKey' && !rsaKey && !hostConfig?.rsaKey) return false; if (form.storePassword) {
if (Boolean(storePassword) && authMethod === 'Select Auth') return false; if (authMethod === 'Select Auth') return false;
if (authMethod === 'password' && !password?.trim()) return false;
if (authMethod === 'key' && !privateKey?.trim()) return false;
}
return true; return true;
}; };
@@ -142,15 +153,25 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
setIsLoading(true); setIsLoading(true);
try { try {
await handleEditHost(hostConfig, { const newConfig = {
name: form.name || form.ip, name: form.name || form.ip,
folder: form.folder, folder: form.folder,
ip: form.ip, ip: form.ip,
user: form.user, user: form.user,
password: form.authMethod === 'password' ? form.password : undefined,
rsaKey: form.authMethod === 'rsaKey' ? form.rsaKey : undefined,
port: String(form.port), port: String(form.port),
}); };
if (form.storePassword) {
if (form.authMethod === 'password') {
newConfig.password = form.password;
} else if (form.authMethod === 'key') {
newConfig.privateKey = form.privateKey;
newConfig.keyType = form.keyType;
newConfig.passphrase = form.passphrase;
}
}
await handleEditHost(hostConfig, newConfig);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -162,11 +183,9 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
open={!isHidden} open={!isHidden}
onClose={() => !isLoading && setIsEditHostHidden(true)} onClose={() => !isLoading && setIsEditHostHidden(true)}
sx={{ sx={{
position: 'fixed',
inset: 0,
display: 'flex', display: 'flex',
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center',
backdropFilter: 'blur(5px)', backdropFilter: 'blur(5px)',
backgroundColor: 'rgba(0, 0, 0, 0.2)', backgroundColor: 'rgba(0, 0, 0, 0.2)',
}} }}
@@ -178,7 +197,7 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
backgroundColor: theme.palette.general.tertiary, backgroundColor: theme.palette.general.tertiary,
borderColor: theme.palette.general.secondary, borderColor: theme.palette.general.secondary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: 3, padding: 0,
borderRadius: 10, borderRadius: 10,
maxWidth: '500px', maxWidth: '500px',
width: '100%', width: '100%',
@@ -188,134 +207,133 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
mx: 2, mx: 2,
}} }}
> >
<DialogTitle sx={{ mb: 2 }}>Edit Host</DialogTitle> <Tabs
<DialogContent> value={activeTab}
<form onSubmit={handleSubmit}> onChange={(e, val) => setActiveTab(val)}
<Tabs sx={{
value={activeTab} width: '100%',
onChange={(e, val) => setActiveTab(val)} mb: 0,
sx={{ backgroundColor: theme.palette.general.tertiary,
backgroundColor: theme.palette.general.disabled, }}
borderRadius: '8px', >
padding: '8px', <TabList
marginBottom: '16px', sx={{
width: '100%', width: '100%',
}} gap: 0,
> borderTopLeftRadius: 10,
<TabList borderTopRightRadius: 10,
sx={{ backgroundColor: theme.palette.general.primary,
width: '100%', '& button': {
gap: 0, flex: 1,
mb: 2, bgcolor: 'transparent',
'& button': { color: theme.palette.text.secondary,
flex: 1, '&:hover': {
bgcolor: 'transparent', bgcolor: theme.palette.general.disabled,
color: theme.palette.text.secondary, },
'&:hover': { '&.Mui-selected': {
bgcolor: 'rgba(255, 255, 255, 0.1)', bgcolor: theme.palette.general.tertiary,
}, color: theme.palette.text.primary,
'&.Mui-selected': { '&:hover': {
bgcolor: theme.palette.general.primary, bgcolor: theme.palette.general.tertiary,
color: theme.palette.text.primary,
'&:hover': {
bgcolor: theme.palette.general.primary,
},
},
}, },
}} },
> },
<Tab>Basic Info</Tab> }}
<Tab>Connection</Tab> >
<Tab>Authentication</Tab> <Tab sx={{ flex: 1 }}>Basic Info</Tab>
</TabList> <Tab sx={{ flex: 1 }}>Connection</Tab>
<Tab sx={{ flex: 1 }}>Authentication</Tab>
</TabList>
<TabPanel value={0}> <div style={{ padding: '24px', backgroundColor: theme.palette.general.tertiary }}>
<Stack spacing={2}> <TabPanel value={0}>
<FormControl> <Stack spacing={2}>
<FormLabel>Host Name</FormLabel> <FormControl>
<Input <FormLabel>Host Name</FormLabel>
value={form.name} <Input
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))} value={form.name}
sx={{ onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
backgroundColor: theme.palette.general.primary, sx={{
color: theme.palette.text.primary backgroundColor: theme.palette.general.primary,
}} color: theme.palette.text.primary
/> }}
</FormControl> />
</FormControl>
<FormControl> <FormControl>
<FormLabel>Folder</FormLabel> <FormLabel>Folder</FormLabel>
<Input <Input
value={form.folder} value={form.folder}
onChange={(e) => setForm((prev) => ({ ...prev, folder: e.target.value }))} onChange={(e) => setForm((prev) => ({ ...prev, folder: e.target.value }))}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary color: theme.palette.text.primary
}} }}
/> />
</FormControl> </FormControl>
</Stack> </Stack>
</TabPanel> </TabPanel>
<TabPanel value={1}> <TabPanel value={1}>
<Stack spacing={2}> <Stack spacing={2}>
<FormControl error={!form.ip}> <FormControl error={!form.ip}>
<FormLabel>Host IP</FormLabel> <FormLabel>Host IP</FormLabel>
<Input <Input
value={form.ip} value={form.ip}
onChange={(e) => setForm((prev) => ({ ...prev, ip: e.target.value }))} onChange={(e) => setForm((prev) => ({ ...prev, ip: e.target.value }))}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary color: theme.palette.text.primary
}} }}
/> />
</FormControl> </FormControl>
<FormControl error={form.port < 1 || form.port > 65535}> <FormControl error={form.port < 1 || form.port > 65535}>
<FormLabel>Host Port</FormLabel> <FormLabel>Host Port</FormLabel>
<Input <Input
type="number" type="number"
value={form.port} value={form.port}
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))} onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary color: theme.palette.text.primary
}} }}
/> />
</FormControl> </FormControl>
<FormControl error={!form.user}> <FormControl error={!form.user}>
<FormLabel>Host User</FormLabel> <FormLabel>Host User</FormLabel>
<Input <Input
value={form.user} value={form.user}
onChange={(e) => setForm((prev) => ({ ...prev, user: e.target.value }))} onChange={(e) => setForm((prev) => ({ ...prev, user: e.target.value }))}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary color: theme.palette.text.primary
}} }}
/> />
</FormControl> </FormControl>
</Stack> </Stack>
</TabPanel> </TabPanel>
<TabPanel value={2}> <TabPanel value={2}>
<Stack spacing={2}> <Stack spacing={2}>
<FormControl> <FormControl>
<FormLabel>Store Password</FormLabel> <FormLabel>Store Password</FormLabel>
<Checkbox <Checkbox
checked={form.storePassword} checked={Boolean(form.storePassword)}
onChange={(e) => handleStorePasswordChange(e.target.checked)} onChange={(e) => handleStorePasswordChange(e.target.checked)}
sx={{ sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary, color: theme.palette.text.primary,
'&.Mui-checked': { },
color: theme.palette.text.primary }}
} />
}} </FormControl>
/>
</FormControl>
{form.storePassword && ( {form.storePassword && (
<FormControl error={form.authMethod === 'Select Auth'}> <>
<FormControl error={form.storePassword && (!form.authMethod || form.authMethod === 'Select Auth')}>
<FormLabel>Authentication Method</FormLabel> <FormLabel>Authentication Method</FormLabel>
<Select <Select
value={form.authMethod} value={form.authMethod}
@@ -330,170 +348,129 @@ const EditHostModal = ({ isHidden, hostConfig, setIsEditHostHidden, handleEditHo
<Option value="key">SSH Key</Option> <Option value="key">SSH Key</Option>
</Select> </Select>
</FormControl> </FormControl>
)}
{form.authMethod === 'password' && form.storePassword && ( {form.authMethod === 'password' && (
<FormControl error={!form.password}> <FormControl error={form.storePassword && !form.password}>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<Input
type={showPassword ? 'text' : 'password'}
value={form.password}
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
{form.authMethod === 'rsaKey' && form.storePassword && (
<FormControl error={!form.rsaKey && !hostConfig?.rsaKey}>
<FormLabel>Public Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.rsaKey ? 'Change Public Key File' : 'Upload Public Key File'}
<Input
type="file"
onChange={handleFileChange}
sx={{ display: 'none' }}
/>
</Button>
{hostConfig?.rsaKey && !form.rsaKey && (
<FormLabel
sx={{
color: theme.palette.text.secondary,
fontSize: '0.875rem',
mt: 1,
display: 'block',
textAlign: 'center'
}}
>
Existing key detected. Upload to replace.
</FormLabel>
)}
</FormControl>
)}
{form.authMethod === 'key' && form.storePassword && (
<Stack spacing={2}>
<FormControl error={!form.privateKey && !hostConfig?.privateKey}>
<FormLabel>SSH Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
},
}}
>
{form.privateKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'}
<Input <Input
type="file" type={showPassword ? 'text' : 'password'}
onChange={handleFileChange} value={form.password}
sx={{ display: 'none' }} onChange={(e) => setForm(prev => ({ ...prev, password: e.target.value }))}
/>
</Button>
{hostConfig?.privateKey && !form.privateKey && (
<FormLabel
sx={{ sx={{
color: theme.palette.text.secondary, backgroundColor: theme.palette.general.primary,
fontSize: '0.875rem', color: theme.palette.text.primary,
mt: 1, flex: 1
display: 'block', }}
textAlign: 'center' />
<IconButton
onClick={() => setShowPassword(!showPassword)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}} }}
> >
Existing {hostConfig.keyType || 'SSH'} key detected. Upload to replace. {showPassword ? <VisibilityOff /> : <Visibility />}
</FormLabel> </IconButton>
)} </div>
</FormControl> </FormControl>
{form.privateKey && ( )}
<FormControl>
<FormLabel>Key Passphrase (optional)</FormLabel> {form.authMethod === 'key' && (
<div style={{ display: 'flex', alignItems: 'center' }}> <Stack spacing={2}>
<FormControl error={form.storePassword && !form.privateKey}>
<FormLabel>SSH Key</FormLabel>
<Button
component="label"
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '40px',
'&:hover': {
backgroundColor: theme.palette.general.disabled,
},
}}
>
{form.privateKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'}
<Input <Input
type={showPassphrase ? "text" : "password"} type="file"
value={form.passphrase || ''} onChange={handleFileChange}
onChange={(e) => setForm(prev => ({ ...prev, passphrase: e.target.value }))} sx={{ display: 'none' }}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/> />
<IconButton </Button>
onClick={() => setShowPassphrase(!showPassphrase)} {hostConfig?.privateKey && !form.privateKey && (
<FormLabel
sx={{ sx={{
color: theme.palette.text.primary, color: theme.palette.text.secondary,
marginLeft: 1 fontSize: '0.875rem',
mt: 1,
display: 'block',
textAlign: 'center'
}} }}
> >
{showPassphrase ? <VisibilityOff /> : <Visibility />} Existing {hostConfig.keyType || 'SSH'} key detected. Upload to replace.
</IconButton> </FormLabel>
</div> )}
</FormControl> </FormControl>
)} {form.privateKey && (
</Stack> <FormControl>
)} <FormLabel>Key Passphrase (optional)</FormLabel>
</Stack> <div style={{ display: 'flex', alignItems: 'center' }}>
</TabPanel> <Input
</Tabs> type={showPassphrase ? "text" : "password"}
value={form.passphrase || ''}
onChange={(e) => setForm(prev => ({ ...prev, passphrase: e.target.value }))}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
flex: 1
}}
/>
<IconButton
onClick={() => setShowPassphrase(!showPassphrase)}
sx={{
color: theme.palette.text.primary,
marginLeft: 1
}}
>
{showPassphrase ? <VisibilityOff /> : <Visibility />}
</IconButton>
</div>
</FormControl>
)}
</Stack>
)}
</>
)}
</Stack>
</TabPanel>
</div>
<Button <Button
type="submit" onClick={handleSubmit}
disabled={!isFormValid() || isLoading} disabled={isLoading}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary, color: theme.palette.text.primary,
'&:hover': { '&:hover': {
backgroundColor: theme.palette.general.disabled backgroundColor: theme.palette.general.disabled,
}, },
'&:disabled': { '&:disabled': {
backgroundColor: 'rgba(255, 255, 255, 0.1)', backgroundColor: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.3)', color: 'rgba(255, 255, 255, 0.3)',
}, },
marginTop: 3, marginTop: 1,
width: '100%', width: '100%',
height: '40px', height: '40px',
}} }}
> >
{isLoading ? "Saving..." : "Save Changes"} {isLoading ? "Saving changes..." : "Save changes"}
</Button> </Button>
</form> </Tabs>
</DialogContent>
</ModalDialog> </ModalDialog>
</Modal> </Modal>
</CssVarsProvider> </CssVarsProvider>