import { useQuery } from "@tanstack/react-query"; import { createApiKey, deleteApiKey, getApiKeys, type ApiKey } from "api/api"; import { AsyncButton } from "../AsyncButton"; import { useEffect, useRef, useState } from "react"; import { Copy, Trash } from "lucide-react"; type CopiedState = { x: number; y: number; visible: boolean; }; export default function ApiKeysModal() { const [input, setInput] = useState('') const [loading, setLoading ] = useState(false) const [err, setError ] = useState() const [displayData, setDisplayData] = useState([]) const [copied, setCopied] = useState(null); const [expandedKey, setExpandedKey] = useState(null); const textRefs = useRef>({}); const handleRevealAndSelect = (key: string) => { setExpandedKey(key); setTimeout(() => { const el = textRefs.current[key]; if (el) { const range = document.createRange(); range.selectNodeContents(el); const sel = window.getSelection(); sel?.removeAllRanges(); sel?.addRange(range); } }, 0); }; const { isPending, isError, data, error } = useQuery({ queryKey: [ 'api-keys' ], queryFn: () => { return getApiKeys(); }, }); useEffect(() => { if (data) { setDisplayData(data) } }, [data]) if (isError) { return (

Error: {error.message}

) } if (isPending) { return (

Loading...

) } const handleCopy = (e: React.MouseEvent, text: string) => { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).catch(() => fallbackCopy(text)); } else { fallbackCopy(text); } const parentRect = (e.currentTarget.closest(".relative") as HTMLElement).getBoundingClientRect(); const buttonRect = e.currentTarget.getBoundingClientRect(); setCopied({ x: buttonRect.left - parentRect.left + buttonRect.width / 2, y: buttonRect.top - parentRect.top - 8, visible: true, }); setTimeout(() => setCopied(null), 1500); }; const fallbackCopy = (text: string) => { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; // prevent scroll to bottom document.body.appendChild(textarea); textarea.focus(); textarea.select(); try { document.execCommand("copy"); } catch (err) { console.error("Fallback: Copy failed", err); } document.body.removeChild(textarea); }; const handleCreateApiKey = () => { setError(undefined) if (input === "") { setError("a label must be provided") return } setLoading(true) createApiKey(input) .then(r => { setDisplayData([r, ...displayData]) setInput('') }).catch((err) => setError(err.message)) setLoading(false) } const handleDeleteApiKey = (id: number) => { setError(undefined) setLoading(true) deleteApiKey(id) .then(r => { if (r.ok) { setDisplayData(displayData.filter((v) => v.id != id)) } else { r.json().then((r) => setError(r.error)) } }) setLoading(false) } return (

API Keys

{displayData.map((v) => (
{ textRefs.current[v.key] = el; }} onClick={() => handleRevealAndSelect(v.key)} className={`bg p-3 rounded-md flex-grow cursor-pointer select-text ${ expandedKey === v.key ? '' : 'truncate' }`} style={{ whiteSpace: 'nowrap' }} title={v.key} // optional tooltip > {expandedKey === v.key ? v.key : `${v.key.slice(0, 8)}... ${v.label}`}
handleDeleteApiKey(v.id)} confirm>
))}
setInput(e.target.value)} /> Create
{err &&

{err}

} {copied?.visible && (
Copied!
)}
) }