import { useState } from "react"; import { useTranslation } from "react-i18next"; import { ChevronRight, Copy, FolderOpen, KeyRound, Loader2, Pencil, Plus, Trash2, Upload, X, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/button"; import { getCredentialDetails } from "@/main-axios"; import { copyToClipboard } from "@/lib/clipboard"; import type { Host, Credential } from "@/types/ui-types"; type CredentialWithCertificate = Credential & { certPublicKey?: string }; type ConfirmDialog = { message: string; onConfirm: () => void; }; function CredentialItem({ cred, usedByCount, stripeIndex, onDeploy, onEdit, onDelete, }: { cred: Credential; usedByCount: number; stripeIndex: number; onDeploy: () => void; onEdit: () => void; onDelete: () => void; }) { const isKey = cred.type === "key"; return (
{/* Type stripe */}
{/* Name row */}
{cred.name} {isKey ? "KEY" : "PWD"}
{/* Username row */} {(cred.username || usedByCount > 0) && ( {cred.username} {usedByCount > 0 && ( {cred.username ? " · " : ""} {usedByCount}h )} )} {/* Tag pills */} {cred.tags && cred.tags.length > 0 && (
{cred.tags.slice(0, 4).map((tag) => ( {tag} ))} {cred.tags.length > 4 && ( +{cred.tags.length - 4} )}
)} {/* Action tray — slides open on hover */}
{isKey && ( <> )}
); } function CredentialFolderItem({ folder, creds, allHosts, stripeOffset, editingFolderName, editingFolderValue, onEditingFolderNameChange, onEditingFolderValueChange, onRenameFolder, onDeploy, onEdit, onDelete, }: { folder: string; creds: Credential[]; allHosts: Host[]; stripeOffset: number; editingFolderName: string | null; editingFolderValue: string; onEditingFolderNameChange: (name: string | null) => void; onEditingFolderValueChange: (value: string) => void; onRenameFolder: (folder: string, newName: string) => Promise; onDeploy: (cred: Credential) => void; onEdit: (cred: Credential) => void; onDelete: (cred: Credential) => void; }) { const [open, setOpen] = useState(false); return (
) : ( <> {folder} {creds.length} {folder !== "Uncategorized" && ( { e.stopPropagation(); onEditingFolderNameChange(folder); onEditingFolderValueChange(folder); }} > )} )} {open && (
{creds.map((cred, i) => { const usedByCount = allHosts.filter( (h) => h.credentialId === cred.id, ).length; return ( onDeploy(cred)} onEdit={() => onEdit(cred)} onDelete={() => onDelete(cred)} /> ); })}
)}
); } export function HostCredentialList({ credentialFolders, filteredCredentials, credentialsLoading, allHosts, editingFolderName, editingFolderValue, onEditingFolderNameChange, onEditingFolderValueChange, onRenameFolder, onDeployCredential, onEditCredential, onDeleteCredential, onAddCredential, onConfirmDialogChange, }: { credentialFolders: string[]; filteredCredentials: Credential[]; credentialsLoading: boolean; allHosts: Host[]; editingFolderName: string | null; editingFolderValue: string; onEditingFolderNameChange: (name: string | null) => void; onEditingFolderValueChange: (value: string) => void; onRenameFolder: (folder: string, newName: string) => Promise; onDeployCredential: (cred: Credential) => void; onEditCredential: (cred: Credential) => void; onDeleteCredential: (cred: Credential) => Promise; onAddCredential: () => void; onConfirmDialogChange: (dialog: ConfirmDialog) => void; }) { const { t } = useTranslation(); async function handleDelete(cred: Credential) { onConfirmDialogChange({ message: t("hosts.deleteCredentialConfirm", { name: cred.name }), onConfirm: async () => { try { await onDeleteCredential(cred); toast.success(t("hosts.deletedCredential", { name: cred.name })); } catch { toast.error(t("hosts.failedToDeleteCredential2")); } }, }); } async function handleEdit(cred: Credential) { try { const full = await getCredentialDetails(Number(cred.id)); onEditCredential({ ...cred, value: ( full as CredentialWithCertificate & { hasKey?: boolean; hasKeyPassword?: boolean; } ).hasKey ? "existing_key" : (( full as CredentialWithCertificate & { password?: string; } ).password ?? ""), passphrase: ( full as CredentialWithCertificate & { hasKeyPassword?: boolean; } ).hasKeyPassword ? "existing_key_password" : "", publicKey: (full as CredentialWithCertificate).certPublicKey ?? cred.publicKey, }); } catch { onEditCredential(cred); } } let globalStripe = 0; return (
{credentialFolders.map((folder) => { const creds = filteredCredentials.filter( (c) => (c.folder || "Uncategorized") === folder, ); if (creds.length === 0) return null; const offset = globalStripe; globalStripe += 1 + creds.length; return ( ); })} {credentialsLoading && (
{[60, 45, 55, 40].map((w, i) => (
))}
{t("hosts.loadingCredentials")}
)} {!credentialsLoading && filteredCredentials.length === 0 && (
{t("hosts.noCredentialsFound")}
)}
); }