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
This commit is contained in:
committed by
GitHub
parent
08b7eecdfe
commit
16e918e9b4
@@ -1376,7 +1376,7 @@ export function InstalledScriptsTab() {
|
||||
type="text"
|
||||
value={editFormData.web_ui_ip}
|
||||
onChange={(e) => handleInputChange('web_ui_ip', e.target.value)}
|
||||
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"
|
||||
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"
|
||||
placeholder="IP"
|
||||
/>
|
||||
<span className="text-muted-foreground">:</span>
|
||||
|
||||
@@ -14,6 +14,7 @@ interface PublicKeyModalProps {
|
||||
|
||||
export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverIp }: PublicKeyModalProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [commandCopied, setCommandCopied] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@@ -54,6 +55,42 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyCommand = async () => {
|
||||
const command = `echo "${publicKey}" >> ~/.ssh/authorized_keys`;
|
||||
try {
|
||||
// Try modern clipboard API first
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCommandCopied(true);
|
||||
setTimeout(() => setCommandCopied(false), 2000);
|
||||
} else {
|
||||
// Fallback for older browsers or non-HTTPS
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = command;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
setCommandCopied(true);
|
||||
setTimeout(() => setCommandCopied(false), 2000);
|
||||
} catch (fallbackError) {
|
||||
console.error('Fallback copy failed:', fallbackError);
|
||||
alert('Please manually copy this command:\n\n' + command);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to copy command to clipboard:', error);
|
||||
alert('Please manually copy this command:\n\n' + command);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<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
|
||||
<textarea
|
||||
value={publicKey}
|
||||
readOnly
|
||||
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"
|
||||
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"
|
||||
placeholder="Public key will appear here..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Command */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Quick Add Command:</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyCommand}
|
||||
className="gap-2"
|
||||
>
|
||||
{commandCopied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy Command
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-3 bg-muted/50 rounded-md border border-border">
|
||||
<code className="text-sm font-mono text-foreground break-all">
|
||||
echo "{publicKey}" >> ~/.ssh/authorized_keys
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Copy and paste this command directly into your server terminal to add the key to authorized_keys
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-border">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
|
||||
@@ -30,7 +30,11 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string;
|
||||
let content = e.target?.result as string;
|
||||
|
||||
// Auto-trim trailing whitespace and empty lines to fix import issues
|
||||
content = content.replace(/\n\s*$/, '').trimEnd();
|
||||
|
||||
if (validateSSHKey(content)) {
|
||||
onChange(content);
|
||||
onError?.('');
|
||||
@@ -72,7 +76,12 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
|
||||
};
|
||||
|
||||
const handlePasteChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const content = event.target.value;
|
||||
let content = event.target.value;
|
||||
|
||||
// Auto-trim trailing whitespace and empty lines to fix import issues
|
||||
// This addresses the common problem where pasted SSH keys have extra whitespace
|
||||
content = content.replace(/\n\s*$/, '').trimEnd();
|
||||
|
||||
onChange(content);
|
||||
|
||||
if (content.trim() && !validateSSHKey(content)) {
|
||||
|
||||
@@ -351,13 +351,25 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
|
||||
{/* Show generated key status */}
|
||||
{formData.key_generated && (
|
||||
<div className="p-3 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-md">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-green-800 dark:text-green-200">
|
||||
SSH key pair generated successfully
|
||||
</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-green-800 dark:text-green-200">
|
||||
SSH key pair generated successfully
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowPublicKeyModal(true)}
|
||||
className="gap-2 border-blue-500/20 text-blue-400 bg-blue-500/10 hover:bg-blue-500/20"
|
||||
>
|
||||
<Key className="h-4 w-4" />
|
||||
View Public Key
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-green-700 dark:text-green-300 mt-1">
|
||||
The private key has been generated and will be saved with the server.
|
||||
|
||||
Reference in New Issue
Block a user