import React from "react"; import { ChevronDown, Eye, EyeOff, FileKey, FolderLock, FolderOpen, Key, KeyRound, MapPin, Plus, Shapes, Shield, Trash2, User, X } from "lucide-react"; import type { Host } from "../types"; import { applyHostAuthMethodSelection } from "../domain/sshAuth"; import { HostIconPicker } from "./HostIconPicker"; import { Button } from "./ui/button"; import { Combobox } from "./ui/combobox"; import { HostDetailsSection, HostDetailsSettingRow } from "./host-details"; import { Input } from "./ui/input"; import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "./ui/popover"; import { ScrollArea } from "./ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Switch } from "./ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type HostDetailsConnectionSectionsProps = Record; export const HOST_AUTH_METHOD_CHOICES = [ ["auto", "hostDetails.auth.auto"], ["password", "hostDetails.auth.passwordOnly"], ["key", "hostDetails.auth.key"], ["certificate", "hostDetails.auth.certificate"], ] as const; export function shouldForceAuthMethodReselect( nextMethod: "auto" | "password" | "key" | "certificate", currentMethod: "auto" | "password" | "key" | "certificate", ): boolean { // Radix Select skips onValueChange when the value is unchanged. Key/certificate // still need that path so re-opening the chooser keeps working. return ( nextMethod === currentMethod && (nextMethod === "key" || nextMethod === "certificate") ); } export const applyEffectiveHostAuthMethodSelection = ( host: Host, authMethod: "auto" | "password" | "key" | "certificate", effectiveAuthMethod: "auto" | "password" | "key" | "certificate", effectiveUsername?: string, ): Host => { if (authMethod === effectiveAuthMethod) return host; const selected = applyHostAuthMethodSelection(host, authMethod, effectiveAuthMethod); return !selected.username?.trim() && effectiveUsername?.trim() ? { ...selected, username: effectiveUsername } : selected; }; export const detachEffectiveHostIdentity = (host: Host, effectiveUsername?: string): Host => ({ ...host, identityId: "", ...(!host.username?.trim() && effectiveUsername?.trim() ? { username: effectiveUsername } : {}), }); export const removeSelectedHostCredential = ( host: Host, effectiveAuthMethod: "auto" | "password" | "key" | "certificate", effectiveUsername?: string, ): Host => { const authMethod = host.password ? "password" : "auto"; const selected = applyHostAuthMethodSelection(host, authMethod, effectiveAuthMethod); return !selected.username?.trim() && effectiveUsername?.trim() ? { ...selected, username: effectiveUsername } : selected; }; export const HostDetailsConnectionSections: React.FC = ({ t, form, setForm, update, groupDefaults, effectiveAuthMethod, effectiveUsername, effectiveIdentityId, selectedIdentity, clearIdentity, identities, identitySuggestionsOpen, filteredIdentitySuggestions, setIdentitySuggestionsOpen, availableKeys, applyIdentity, showPassword, setShowPassword, pendingReferenceKeyPath, setPendingReferenceKeyPath, selectedCredentialType, setSelectedCredentialType, credentialPopoverOpen, setCredentialPopoverOpen, keysByCategory, newKeyFilePath, setNewKeyFilePath, addLocalKeyFilePath, distroOptions, effectiveFormDistro, getDistroOptionLabel, }) => { const selectAuthMethod = (authMethod: "auto" | "password" | "key" | "certificate") => { setForm((previous: Host) => applyEffectiveHostAuthMethodSelection( previous, authMethod, effectiveAuthMethod, effectiveUsername, )); if (authMethod === "auto" || authMethod === "password") { setPendingReferenceKeyPath(null); setSelectedCredentialType(null); setCredentialPopoverOpen(false); return; } setSelectedCredentialType(authMethod); setCredentialPopoverOpen(false); }; const resolvedAuthMethod = HOST_AUTH_METHOD_CHOICES.some(([value]) => value === effectiveAuthMethod) ? effectiveAuthMethod : "auto"; const selectedAuthMethodLabel = t( HOST_AUTH_METHOD_CHOICES.find(([value]) => value === resolvedAuthMethod)?.[1] ?? "hostDetails.auth.auto", ); const effectiveEtEnabled = form.etEnabled ?? groupDefaults?.etEnabled; const effectiveProtocol = form.protocol ?? groupDefaults?.protocol; return ( <> } title={t("hostDetails.section.address")} > update("hostname", e.target.value)} className="h-10 w-full" /> } title={t("hostDetails.section.portCredentials")} className="overflow-hidden" >
SSH on
update("port", e.target.value ? Number(e.target.value) : undefined)} placeholder={groupDefaults?.port ? String(groupDefaults.port) : "22"} className="h-8 flex-1 min-w-0 text-center" /> {t("hostDetails.port")}
{!effectiveEtEnabled && effectiveProtocol !== "et" && ( update("requiresMfa" as keyof Host, val)} /> )} {selectedIdentity ? (
{selectedIdentity.label}
{t("common.clear")}
) : effectiveIdentityId ? (
{t("hostDetails.identity.missing")}
{t("common.clear")}
) : ( (() => { const hasIdentities = identities.length > 0; if (!hasIdentities) { return ( update("username", e.target.value)} className="h-10" /> ); } return ( 0 } onOpenChange={setIdentitySuggestionsOpen} > {/* Anchor (not Trigger): wrapping the username Input in PopoverTrigger races focus→open with the same click's toggle→close, which flashes saved identities then closes. Anchor is not exempt from Radix outside dismissal (only Trigger is), so tag it and ignore those events below. */}
{ const next = e.target.value; update("username", next); const q = next.toLowerCase().trim(); const matches = q ? identities.filter( (i) => i.label.toLowerCase().includes(q) || i.username.toLowerCase().includes(q), ) : identities; setIdentitySuggestionsOpen(matches.length > 0); }} onFocus={() => { const q = (form.username || "").toLowerCase().trim(); const matches = q ? identities.filter( (i) => i.label.toLowerCase().includes(q) || i.username.toLowerCase().includes(q), ) : identities; setIdentitySuggestionsOpen(matches.length > 0); }} className="h-10 pr-9" /> {t("hostDetails.identity.suggestions")}
e.preventDefault()} onInteractOutside={(e) => { const target = e.target as Element | null; if (target?.closest("[data-identity-suggestions-anchor]")) { e.preventDefault(); } }} style={{ width: "var(--radix-popover-trigger-width)" }} >
{filteredIdentitySuggestions.length === 0 ? (
{t("common.noResultsFound")}
) : (
{filteredIdentitySuggestions.map((identity) => { const keyLabel = identity.keyId ? availableKeys.find( (k) => k.id === identity.keyId, )?.label : undefined; const methodLabel = identity.authMethod === "certificate" ? t("hostDetails.credential.certificate") : identity.authMethod === "key" ? t("hostDetails.credential.key") : t("keychain.identity.method.passwordOnly"); const summaryParts = [ identity.username, identity.password ? "******" : undefined, keyLabel, ].filter(Boolean); return ( ); })}
)}
); })() )} {!selectedIdentity && !form.identityId && (
update("password", e.target.value)} className="h-10 pr-10" /> {showPassword ? t("hostDetails.password.hide") : t("hostDetails.password.show")}
)} {/* Save Password toggle - shown when password is entered */} {!selectedIdentity && !form.identityId && form.password && (
{t("hostDetails.password.save")} update("savePassword" as keyof Host, val)} />
)} {/* Local key file paths display */} {!selectedIdentity && !form.identityFileId && form.identityFilePaths && form.identityFilePaths.length > 0 && (
{form.identityFilePaths.map((keyPath, idx) => (
{keyPath} {keyPath}
))}
)} {/* Selected credential display */} {!selectedIdentity && form.identityFileId && (
{form.authMethod === "certificate" ? ( ) : ( )} {availableKeys.find((k) => k.id === form.identityFileId) ?.label || "Key"}
)} {/* Credential type selection with inline popover - hidden when credential is selected */} {!selectedIdentity && !form.identityFileId && !selectedCredentialType && (
)} {/* Key selection combobox - appears after selecting "Key" type */} {!selectedIdentity && selectedCredentialType === "key" && !form.identityFileId && (
({ value: k.id, label: k.label, sublabel: `${k.type}${k.keySize ? ` ${k.keySize}` : ""}`, icon: , }))} value={form.identityFileId} onValueChange={(val) => { update("identityFileId", val); update("authMethod", "key"); update("identityFilePaths", undefined); setPendingReferenceKeyPath(null); setSelectedCredentialType(null); }} placeholder={t("hostDetails.keys.search")} emptyText={t("hostDetails.keys.empty")} icon={} className="flex-1" />
)} {/* Certificate selection combobox - appears after selecting "Certificate" type */} {!selectedIdentity && selectedCredentialType === "certificate" && !form.identityFileId && (
({ value: k.id, label: k.label, icon: ( ), }))} value={form.identityFileId} onValueChange={(val) => { update("identityFileId", val); update("authMethod", "certificate"); update("identityFilePaths", undefined); setPendingReferenceKeyPath(null); setSelectedCredentialType(null); }} placeholder={t("hostDetails.certs.search")} emptyText={t("hostDetails.certs.empty")} icon={ } className="flex-1" />
)} {/* Local key file path input - appears after selecting "Local Key File" type */} {!selectedIdentity && selectedCredentialType === "localKeyFile" && !form.identityFileId && (
setNewKeyFilePath(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && newKeyFilePath.trim()) { e.preventDefault(); addLocalKeyFilePath(newKeyFilePath); } }} /> {t("hostDetails.credential.browseKeyFile")}
)}
} title={t("hostDetails.section.sftp")} > update("sftpSudo", val)} disabled={form.sftpFileProtocol === "scp"} /> {form.sftpSudo && !form.password && !selectedIdentity?.password && (

{t("hostDetails.sftp.sudo.passwordWarning")}

)}
} title={t("hostDetails.icon.sectionTitle")} hint={t("hostDetails.icon.desc")} > { if ("distroMode" in next) update("distroMode", next.distroMode); if ("manualDistro" in next) update("manualDistro", next.manualDistro); if ("iconMode" in next) update("iconMode", next.iconMode); if ("iconId" in next) update("iconId", next.iconId); if ("iconColorMode" in next) update("iconColorMode", next.iconColorMode); if ("iconColor" in next) update("iconColor", next.iconColor); if ("iconColorCustom" in next) update("iconColorCustom", next.iconColorCustom); }} /> ); };