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
268 lines
12 KiB
TypeScript
268 lines
12 KiB
TypeScript
/**
|
|
* Identity Panel - Create/Edit identity
|
|
*/
|
|
|
|
import { Eye,EyeOff,Key,Plus,Shield,User,X } from 'lucide-react';
|
|
import React,{ useMemo,useState } from 'react';
|
|
import { useI18n } from '../../application/i18n/I18nProvider';
|
|
import { Identity,SSHKey } from '../../types';
|
|
import { Button } from '../ui/button';
|
|
import { Combobox } from '../ui/combobox';
|
|
import { Input } from '../ui/input';
|
|
import { Label } from '../ui/label';
|
|
import { Popover,PopoverContent,PopoverTrigger } from '../ui/popover';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
|
|
|
interface IdentityPanelProps {
|
|
draftIdentity: Partial<Identity>;
|
|
setDraftIdentity: (identity: Partial<Identity>) => void;
|
|
keys: SSHKey[];
|
|
showPassphrase: boolean;
|
|
setShowPassphrase: (show: boolean) => void;
|
|
isNew: boolean;
|
|
onSave: () => void;
|
|
}
|
|
|
|
export const IdentityPanel: React.FC<IdentityPanelProps> = ({
|
|
draftIdentity,
|
|
setDraftIdentity,
|
|
keys,
|
|
showPassphrase,
|
|
setShowPassphrase,
|
|
isNew,
|
|
onSave,
|
|
}) => {
|
|
const { t } = useI18n();
|
|
|
|
type CredentialType = 'key' | 'certificate' | null;
|
|
const [credentialPopoverOpen, setCredentialPopoverOpen] = useState(false);
|
|
const [selectedCredentialType, setSelectedCredentialType] = useState<CredentialType>(null);
|
|
|
|
const selectedKey = useMemo(() => {
|
|
if (!draftIdentity.keyId) return undefined;
|
|
return keys.find(k => k.id === draftIdentity.keyId);
|
|
}, [draftIdentity.keyId, keys]);
|
|
|
|
const keysByCategory = useMemo(() => {
|
|
return {
|
|
key: keys.filter(k => k.category === 'key' && !k.certificate),
|
|
certificate: keys.filter(k => k.category === 'certificate' || !!k.certificate),
|
|
};
|
|
}, [keys]);
|
|
|
|
const clearSelectedKey = () => {
|
|
setDraftIdentity({
|
|
...draftIdentity,
|
|
keyId: undefined,
|
|
authMethod: 'password',
|
|
});
|
|
setSelectedCredentialType(null);
|
|
setCredentialPopoverOpen(false);
|
|
};
|
|
|
|
const setSelectedKeyId = (keyId: string, kind: Exclude<CredentialType, null>) => {
|
|
setDraftIdentity({
|
|
...draftIdentity,
|
|
keyId: keyId || undefined,
|
|
authMethod: kind === 'certificate' ? 'certificate' : 'key',
|
|
});
|
|
setSelectedCredentialType(null);
|
|
setCredentialPopoverOpen(false);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="h-10 w-10 rounded-lg bg-emerald-600 text-white dark:bg-emerald-400 dark:text-slate-950 flex items-center justify-center">
|
|
<User size={20} />
|
|
</div>
|
|
<Input
|
|
value={draftIdentity.label || ''}
|
|
onChange={e => setDraftIdentity({ ...draftIdentity, label: e.target.value })}
|
|
placeholder={t('keychain.field.label')}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>{t('keychain.identity.usernameRequired')}</Label>
|
|
<div className="relative">
|
|
<User size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
value={draftIdentity.username || ''}
|
|
onChange={e => setDraftIdentity({ ...draftIdentity, username: e.target.value })}
|
|
placeholder={t('terminal.auth.username')}
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>{t('terminal.auth.passwordLabel')}</Label>
|
|
<div className="relative">
|
|
<Input
|
|
type={showPassphrase ? 'text' : 'password'}
|
|
value={draftIdentity.password || ''}
|
|
onChange={e => setDraftIdentity({ ...draftIdentity, password: e.target.value })}
|
|
placeholder={t('terminal.auth.password.placeholder')}
|
|
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>
|
|
|
|
{/* Selected credential display */}
|
|
{draftIdentity.keyId && (
|
|
<div className="flex items-center gap-2 p-2 rounded-md bg-secondary/50 border border-border/60">
|
|
{draftIdentity.authMethod === 'certificate' ? (
|
|
<Shield size={14} className="text-primary" />
|
|
) : (
|
|
<Key size={14} className="text-primary" />
|
|
)}
|
|
<span className="text-sm flex-1 truncate">
|
|
{selectedKey?.label || t('hostDetails.credential.missing')}
|
|
</span>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
onClick={clearSelectedKey}
|
|
>
|
|
<X size={12} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{t('common.clear')}</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
|
|
{/* Credential type selection with inline popover */}
|
|
{!draftIdentity.keyId && !selectedCredentialType && (
|
|
<Popover open={credentialPopoverOpen} onOpenChange={setCredentialPopoverOpen}>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors py-1"
|
|
>
|
|
<Plus size={12} />
|
|
<span>{t('hostDetails.credential.keyCertificate')}</span>
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-[200px] p-1" align="start" sideOffset={4}>
|
|
<div className="space-y-0.5">
|
|
<button
|
|
type="button"
|
|
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-secondary/80 transition-colors text-left"
|
|
onClick={() => {
|
|
setSelectedCredentialType('key');
|
|
setCredentialPopoverOpen(false);
|
|
}}
|
|
>
|
|
<Key size={16} className="text-muted-foreground" />
|
|
<span className="text-sm font-medium">{t('hostDetails.credential.key')}</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-secondary/80 transition-colors text-left"
|
|
onClick={() => {
|
|
setSelectedCredentialType('certificate');
|
|
setCredentialPopoverOpen(false);
|
|
}}
|
|
>
|
|
<Shield size={16} className="text-muted-foreground" />
|
|
<span className="text-sm font-medium">
|
|
{t('hostDetails.credential.certificate')}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
|
|
{/* Key selection combobox - appears after selecting "Key" type */}
|
|
{selectedCredentialType === 'key' && !draftIdentity.keyId && (
|
|
<div className="flex items-center gap-1">
|
|
<Combobox
|
|
options={keysByCategory.key.map(k => ({
|
|
value: k.id,
|
|
label: k.label,
|
|
sublabel: `${k.type}${k.keySize ? ` ${k.keySize}` : ''}`,
|
|
icon: <Key size={14} className="text-muted-foreground" />,
|
|
}))}
|
|
value={draftIdentity.keyId}
|
|
onValueChange={(val) => setSelectedKeyId(val, 'key')}
|
|
placeholder={t('hostDetails.keys.search')}
|
|
emptyText={t('hostDetails.keys.empty')}
|
|
icon={<Key size={14} className="text-muted-foreground" />}
|
|
className="flex-1"
|
|
/>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 shrink-0"
|
|
onClick={() => setSelectedCredentialType(null)}
|
|
>
|
|
<X size={14} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{t('common.cancel')}</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
|
|
{/* Certificate selection combobox */}
|
|
{selectedCredentialType === 'certificate' && !draftIdentity.keyId && (
|
|
<div className="flex items-center gap-1">
|
|
<Combobox
|
|
options={keysByCategory.certificate.map(k => ({
|
|
value: k.id,
|
|
label: k.label,
|
|
icon: <Shield size={14} className="text-muted-foreground" />,
|
|
}))}
|
|
value={draftIdentity.keyId}
|
|
onValueChange={(val) => setSelectedKeyId(val, 'certificate')}
|
|
placeholder={t('hostDetails.certs.search')}
|
|
emptyText={t('hostDetails.certs.empty')}
|
|
icon={<Shield size={14} className="text-muted-foreground" />}
|
|
className="flex-1"
|
|
/>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 shrink-0"
|
|
onClick={() => setSelectedCredentialType(null)}
|
|
>
|
|
<X size={14} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{t('common.cancel')}</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
className="w-full h-11"
|
|
onClick={onSave}
|
|
disabled={!draftIdentity.label?.trim() || !draftIdentity.username?.trim()}
|
|
>
|
|
{isNew ? t('keychain.identity.save') : t('keychain.identity.update')}
|
|
</Button>
|
|
</>
|
|
);
|
|
};
|