Skip to content

Commit 16e918e

Browse files
fix: improve SSH key handling and public key modal UX (#178)
* fix: increase IP input box width in installedScripts table - Changed IP input field width from w-32 (128px) to w-40 (160px) - Fixes truncation issue for IP addresses in format 123.123.123.123 - Affects Web UI column in desktop table view when editing scripts * fix: improve SSH key handling and public key modal UX - Fix SSH key import to automatically trim trailing whitespace and empty lines - Add 'View Public Key' button in ServerForm for generated key pairs - Reduce public key textarea size from 120px to 60px min-height - Add quick command section with pre-filled echo command for authorized_keys - Improve user experience with one-click copy functionality for both key and command
1 parent 08b7eec commit 16e918e

File tree

4 files changed

+102
-11
lines changed

4 files changed

+102
-11
lines changed

src/app/_components/InstalledScriptsTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1376,7 +1376,7 @@ export function InstalledScriptsTab() {
13761376
type="text"
13771377
value={editFormData.web_ui_ip}
13781378
onChange={(e) => handleInputChange('web_ui_ip', e.target.value)}
1379-
className="w-32 px-3 py-2 text-sm font-mono border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
1379+
className="w-40 px-3 py-2 text-sm font-mono border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
13801380
placeholder="IP"
13811381
/>
13821382
<span className="text-muted-foreground">:</span>

src/app/_components/PublicKeyModal.tsx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface PublicKeyModalProps {
1414

1515
export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverIp }: PublicKeyModalProps) {
1616
const [copied, setCopied] = useState(false);
17+
const [commandCopied, setCommandCopied] = useState(false);
1718

1819
if (!isOpen) return null;
1920

@@ -54,6 +55,42 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
5455
}
5556
};
5657

58+
const handleCopyCommand = async () => {
59+
const command = `echo "${publicKey}" >> ~/.ssh/authorized_keys`;
60+
try {
61+
// Try modern clipboard API first
62+
if (navigator.clipboard && window.isSecureContext) {
63+
await navigator.clipboard.writeText(command);
64+
setCommandCopied(true);
65+
setTimeout(() => setCommandCopied(false), 2000);
66+
} else {
67+
// Fallback for older browsers or non-HTTPS
68+
const textArea = document.createElement('textarea');
69+
textArea.value = command;
70+
textArea.style.position = 'fixed';
71+
textArea.style.left = '-999999px';
72+
textArea.style.top = '-999999px';
73+
document.body.appendChild(textArea);
74+
textArea.focus();
75+
textArea.select();
76+
77+
try {
78+
document.execCommand('copy');
79+
setCommandCopied(true);
80+
setTimeout(() => setCommandCopied(false), 2000);
81+
} catch (fallbackError) {
82+
console.error('Fallback copy failed:', fallbackError);
83+
alert('Please manually copy this command:\n\n' + command);
84+
}
85+
86+
document.body.removeChild(textArea);
87+
}
88+
} catch (error) {
89+
console.error('Failed to copy command to clipboard:', error);
90+
alert('Please manually copy this command:\n\n' + command);
91+
}
92+
};
93+
5794
return (
5895
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
5996
<div className="bg-card rounded-lg shadow-xl max-w-2xl w-full border border-border">
@@ -129,11 +166,44 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
129166
<textarea
130167
value={publicKey}
131168
readOnly
132-
className="w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground font-mono text-xs min-h-[120px] resize-none border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
169+
className="w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground font-mono text-xs min-h-[60px] resize-none border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
133170
placeholder="Public key will appear here..."
134171
/>
135172
</div>
136173

174+
{/* Quick Command */}
175+
<div className="space-y-2">
176+
<div className="flex items-center justify-between">
177+
<label className="text-sm font-medium text-foreground">Quick Add Command:</label>
178+
<Button
179+
variant="outline"
180+
size="sm"
181+
onClick={handleCopyCommand}
182+
className="gap-2"
183+
>
184+
{commandCopied ? (
185+
<>
186+
<Check className="h-4 w-4" />
187+
Copied!
188+
</>
189+
) : (
190+
<>
191+
<Copy className="h-4 w-4" />
192+
Copy Command
193+
</>
194+
)}
195+
</Button>
196+
</div>
197+
<div className="p-3 bg-muted/50 rounded-md border border-border">
198+
<code className="text-sm font-mono text-foreground break-all">
199+
echo &quot;{publicKey}&quot; &gt;&gt; ~/.ssh/authorized_keys
200+
</code>
201+
</div>
202+
<p className="text-xs text-muted-foreground">
203+
Copy and paste this command directly into your server terminal to add the key to authorized_keys
204+
</p>
205+
</div>
206+
137207
{/* Footer */}
138208
<div className="flex justify-end gap-3 pt-4 border-t border-border">
139209
<Button variant="outline" onClick={onClose}>

src/app/_components/SSHKeyInput.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
3030

3131
const reader = new FileReader();
3232
reader.onload = (e) => {
33-
const content = e.target?.result as string;
33+
let content = e.target?.result as string;
34+
35+
// Auto-trim trailing whitespace and empty lines to fix import issues
36+
content = content.replace(/\n\s*$/, '').trimEnd();
37+
3438
if (validateSSHKey(content)) {
3539
onChange(content);
3640
onError?.('');
@@ -72,7 +76,12 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
7276
};
7377

7478
const handlePasteChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
75-
const content = event.target.value;
79+
let content = event.target.value;
80+
81+
// Auto-trim trailing whitespace and empty lines to fix import issues
82+
// This addresses the common problem where pasted SSH keys have extra whitespace
83+
content = content.replace(/\n\s*$/, '').trimEnd();
84+
7685
onChange(content);
7786

7887
if (content.trim() && !validateSSHKey(content)) {

src/app/_components/ServerForm.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -351,13 +351,25 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
351351
{/* Show generated key status */}
352352
{formData.key_generated && (
353353
<div className="p-3 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-md">
354-
<div className="flex items-center gap-2">
355-
<svg className="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
356-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
357-
</svg>
358-
<span className="text-sm font-medium text-green-800 dark:text-green-200">
359-
SSH key pair generated successfully
360-
</span>
354+
<div className="flex items-center justify-between">
355+
<div className="flex items-center gap-2">
356+
<svg className="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
357+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
358+
</svg>
359+
<span className="text-sm font-medium text-green-800 dark:text-green-200">
360+
SSH key pair generated successfully
361+
</span>
362+
</div>
363+
<Button
364+
type="button"
365+
variant="outline"
366+
size="sm"
367+
onClick={() => setShowPublicKeyModal(true)}
368+
className="gap-2 border-blue-500/20 text-blue-400 bg-blue-500/10 hover:bg-blue-500/20"
369+
>
370+
<Key className="h-4 w-4" />
371+
View Public Key
372+
</Button>
361373
</div>
362374
<p className="text-xs text-green-700 dark:text-green-300 mt-1">
363375
The private key has been generated and will be saved with the server.

0 commit comments

Comments
 (0)