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 (
{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")}
{t("hosts.addCredentialBtn2")}
)}
);
}