[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
201
components/keychain/ImportKeyPanel.tsx
Normal file
201
components/keychain/ImportKeyPanel.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Import Key Panel - Import existing SSH key
|
||||
*/
|
||||
|
||||
import { Eye, EyeOff, Upload } from 'lucide-react';
|
||||
import React,{ useCallback,useRef } from 'react';
|
||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||
import { SSHKey } from '../../types';
|
||||
import { Button } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { detectKeyType } from './utils';
|
||||
|
||||
interface ImportKeyPanelProps {
|
||||
draftKey: Partial<SSHKey>;
|
||||
setDraftKey: (key: Partial<SSHKey>) => void;
|
||||
showPassphrase: boolean;
|
||||
setShowPassphrase: (show: boolean) => void;
|
||||
onImport: () => void;
|
||||
}
|
||||
|
||||
export const ImportKeyPanel: React.FC<ImportKeyPanelProps> = ({
|
||||
draftKey,
|
||||
setDraftKey,
|
||||
showPassphrase,
|
||||
setShowPassphrase,
|
||||
onImport,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileImport = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string;
|
||||
if (content) {
|
||||
const detectedType = detectKeyType(content);
|
||||
const label = file.name.replace(/\.(pem|key|pub|ppk)$/i, '');
|
||||
|
||||
setDraftKey({
|
||||
...draftKey,
|
||||
privateKey: content,
|
||||
label: draftKey.label || label,
|
||||
type: detectedType,
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
event.target.value = '';
|
||||
}, [draftKey, setDraftKey]);
|
||||
|
||||
const handleDrop = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const file = event.dataTransfer.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string;
|
||||
if (content) {
|
||||
const detectedType = detectKeyType(content);
|
||||
const label = file.name.replace(/\.(pem|key|pub|ppk)$/i, '');
|
||||
|
||||
setDraftKey({
|
||||
...draftKey,
|
||||
privateKey: content,
|
||||
label: draftKey.label || label,
|
||||
type: detectedType,
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}, [draftKey, setDraftKey]);
|
||||
|
||||
const handleDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={handleFileImport}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t('keychain.field.label')}</Label>
|
||||
<Input
|
||||
value={draftKey.label || ''}
|
||||
onChange={e => setDraftKey({ ...draftKey, label: e.target.value })}
|
||||
placeholder={t('keychain.field.labelPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t('keychain.field.privateKeyRequired')}</Label>
|
||||
<Textarea
|
||||
value={draftKey.privateKey || ''}
|
||||
onChange={e => setDraftKey({ ...draftKey, privateKey: e.target.value })}
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||
className="min-h-[120px] font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t('keychain.field.publicKey')}</Label>
|
||||
<Textarea
|
||||
value={draftKey.publicKey || ''}
|
||||
onChange={e => setDraftKey({ ...draftKey, publicKey: e.target.value })}
|
||||
placeholder="ssh-ed25519 AAAAC3... user@host"
|
||||
className="min-h-[80px] font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
{t('terminal.auth.certificate')}
|
||||
<span className="text-[10px] px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
|
||||
{t('common.optional')}
|
||||
</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
value={draftKey.certificate || ''}
|
||||
onChange={e => setDraftKey({ ...draftKey, certificate: e.target.value })}
|
||||
placeholder={t('keychain.field.certificatePlaceholder')}
|
||||
className="min-h-[80px] font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t('terminal.auth.passphrase')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassphrase ? 'text' : 'password'}
|
||||
value={draftKey.passphrase || ''}
|
||||
onChange={e => setDraftKey({ ...draftKey, passphrase: e.target.value })}
|
||||
placeholder={t('keychain.generate.passphrasePlaceholder')}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8"
|
||||
onClick={() => setShowPassphrase(!showPassphrase)}
|
||||
>
|
||||
{showPassphrase ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="savePassphraseImport"
|
||||
checked={draftKey.savePassphrase || false}
|
||||
onChange={e => setDraftKey({ ...draftKey, savePassphrase: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<Label htmlFor="savePassphraseImport" className="text-sm font-normal cursor-pointer">
|
||||
{t('keychain.generate.savePassphrase')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="border border-dashed border-border/80 rounded-xl p-4 text-center space-y-2 bg-background/60 transition-colors hover:border-primary/50"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2 text-muted-foreground">
|
||||
<Upload size={16} />
|
||||
<span className="text-sm">{t('keychain.import.dropHint')}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
{t('keychain.import.importFromFile')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full h-11"
|
||||
onClick={onImport}
|
||||
disabled={!draftKey.label?.trim() || !draftKey.privateKey?.trim()}
|
||||
>
|
||||
{t('keychain.import.saveKey')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user