From d717396619b899dbdef87f460c83fd11fe24ff19 Mon Sep 17 00:00:00 2001 From: Gabe Farrell Date: Thu, 12 Jun 2025 01:56:15 -0400 Subject: [PATCH] feat: click to select api keys for http compatibility --- client/app/components/modals/ApiKeysModal.tsx | 61 ++++++++++++++++--- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/client/app/components/modals/ApiKeysModal.tsx b/client/app/components/modals/ApiKeysModal.tsx index 43e242e..a4bd822 100644 --- a/client/app/components/modals/ApiKeysModal.tsx +++ b/client/app/components/modals/ApiKeysModal.tsx @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { createApiKey, deleteApiKey, getApiKeys, type ApiKey } from "api/api"; import { AsyncButton } from "../AsyncButton"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Copy, Trash } from "lucide-react"; type CopiedState = { @@ -16,6 +16,22 @@ export default function ApiKeysModal() { 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: [ @@ -44,19 +60,38 @@ export default function ApiKeysModal() { } const handleCopy = (e: React.MouseEvent, text: string) => { - navigator.clipboard.writeText(text); + 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, // center of button - y: buttonRect.top - parentRect.top - 8, // above the button + 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) @@ -93,8 +128,20 @@ export default function ApiKeysModal() {

API Keys

{displayData.map((v) => ( -
-
{v.key.slice(0, 8)+'...'} {v.label}
+
{ + 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>