From 62a7c89b814080fb1558d9719882bdb3b3482a47 Mon Sep 17 00:00:00 2001 From: Suresh Chaudhary Date: Sat, 28 Mar 2026 12:46:08 +0530 Subject: [PATCH 1/3] fix/ui-fixes-on-routing-rules-sheet --- ui/app/workspace/logs/sheets/logDetailsSheet.tsx | 6 +++--- ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx | 6 +++--- ui/app/workspace/plugins/sheets/pluginSequenceSheet.tsx | 2 +- .../routing-rules/components/celBuilder/celRuleBuilder.tsx | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/app/workspace/logs/sheets/logDetailsSheet.tsx b/ui/app/workspace/logs/sheets/logDetailsSheet.tsx index de1d5d712e..7bc64fc9df 100644 --- a/ui/app/workspace/logs/sheets/logDetailsSheet.tsx +++ b/ui/app/workspace/logs/sheets/logDetailsSheet.tsx @@ -177,17 +177,17 @@ export function LogDetailSheet({ log, open, onOpenChange, handleDelete, onNaviga
- -
- diff --git a/ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx b/ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx index cc43f36e85..37bb027691 100644 --- a/ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx +++ b/ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx @@ -83,17 +83,17 @@ export function MCPLogDetailSheet({ log, open, onOpenChange, handleDelete, onNav
- -
- diff --git a/ui/app/workspace/plugins/sheets/pluginSequenceSheet.tsx b/ui/app/workspace/plugins/sheets/pluginSequenceSheet.tsx index 900c8128f4..eb87819741 100644 --- a/ui/app/workspace/plugins/sheets/pluginSequenceSheet.tsx +++ b/ui/app/workspace/plugins/sheets/pluginSequenceSheet.tsx @@ -177,7 +177,7 @@ export default function PluginSequenceSheet({ open, onClose, plugins }: PluginSe - diff --git a/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx b/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx index 801e544e89..94914de067 100644 --- a/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx +++ b/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx @@ -122,7 +122,7 @@ export function CELRuleBuilder({
-
-
{ if (!disabled && !editMode) setEditMode(true); }} className={!disabled && !editMode ? "cursor-text" : ""}> +
{ if (!disabled && !editMode && !(e.target as HTMLElement).closest("button, a, [role='button']")) setEditMode(true); }} className={!disabled && !editMode ? "cursor-text" : ""}> {editMode ? ( { - if (!disabled) setEditMode(true); + onClick={(e) => { + if (disabled || editMode) return; + if ((e.target as HTMLElement).closest("button, a, [role='button']")) return; + setEditMode(true); }} > diff --git a/ui/components/prompts/components/messagesView/userMessageView.tsx b/ui/components/prompts/components/messagesView/userMessageView.tsx index fc8da8965f..1513d9b6f1 100644 --- a/ui/components/prompts/components/messagesView/userMessageView.tsx +++ b/ui/components/prompts/components/messagesView/userMessageView.tsx @@ -286,8 +286,10 @@ export function UserMessageView({ ) : (
{ - if (!disabled) setEditMode(true); + onClick={(e) => { + if (disabled || editMode) return; + if ((e.target as HTMLElement).closest("button, a, [role='button']")) return; + setEditMode(true); }} > From fd6ed11a42c561484555e100c6895bccda51d4bd Mon Sep 17 00:00:00 2001 From: Suresh Chaudhary Date: Mon, 30 Mar 2026 12:18:00 +0530 Subject: [PATCH 3/3] refactor/Moved copy-to-clipboard functionality to a centralized hook --- .../components/api-keys/apiKeysIndexView.tsx | 7 ++-- .../workspace/logs/sheets/logDetailsSheet.tsx | 23 ++++--------- .../workspace/logs/views/collapsibleBox.tsx | 13 ++----- ui/app/workspace/logs/views/emptyState.tsx | 9 ++--- .../workspace/mcp-logs/views/emptyState.tsx | 9 ++--- .../fragments/prometheusFormFragment.tsx | 7 ++-- .../components/celBuilder/celRuleBuilder.tsx | 9 ++--- .../virtual-keys/views/virtualKeysTable.tsx | 6 ++-- ui/components/ui/input.tsx | 12 +++---- ui/hooks/useCopyToClipboard.ts | 34 +++++++++++++++++++ 10 files changed, 65 insertions(+), 64 deletions(-) create mode 100644 ui/hooks/useCopyToClipboard.ts diff --git a/ui/app/_fallbacks/enterprise/components/api-keys/apiKeysIndexView.tsx b/ui/app/_fallbacks/enterprise/components/api-keys/apiKeysIndexView.tsx index 7929d9bea0..8687f4490a 100644 --- a/ui/app/_fallbacks/enterprise/components/api-keys/apiKeysIndexView.tsx +++ b/ui/app/_fallbacks/enterprise/components/api-keys/apiKeysIndexView.tsx @@ -3,10 +3,10 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { useGetCoreConfigQuery } from "@/lib/store"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { Copy, InfoIcon, KeyRound } from "lucide-react"; import Link from "next/link"; import { useMemo } from "react"; -import { toast } from "sonner"; import ContactUsView from "../views/contactUsView"; export default function APIKeysView() { @@ -31,10 +31,7 @@ curl --location 'http://localhost:8080/v1/chat/completions' ] }'`; - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); - toast.success("Copied to clipboard"); - }; + const { copy: copyToClipboard } = useCopyToClipboard(); if (isLoading) { return
Loading...
; diff --git a/ui/app/workspace/logs/sheets/logDetailsSheet.tsx b/ui/app/workspace/logs/sheets/logDetailsSheet.tsx index 7bc64fc9df..74669ec3ca 100644 --- a/ui/app/workspace/logs/sheets/logDetailsSheet.tsx +++ b/ui/app/workspace/logs/sheets/logDetailsSheet.tsx @@ -29,6 +29,7 @@ import { StatusColors, } from "@/lib/constants/logs"; import { LogEntry } from "@/lib/types/logs"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { ChevronDown, ChevronUp, Clipboard, Loader2, MoreVertical, Trash2 } from "lucide-react"; import moment from "moment"; import { toast } from "sonner"; @@ -82,6 +83,8 @@ const isContainerOperation = (object: string) => { }; export function LogDetailSheet({ log, open, onOpenChange, handleDelete, onNavigate, hasPrev = false, hasNext = false }: LogDetailSheetProps) { + const { copy: copyRequestId } = useCopyToClipboard({ successMessage: "Request ID copied" }); + const { copy: copyBody } = useCopyToClipboard({ successMessage: "Request body copied to clipboard", errorMessage: "Failed to copy request body" }); const [fetchLog, { data: fullLog, isFetching }] = useLazyGetLogByIdQuery(); useEffect(() => { @@ -147,12 +150,7 @@ export function LogDetailSheet({ log, open, onOpenChange, handleDelete, onNaviga Request ID:{" "} { - navigator.clipboard - .writeText(displayLog.id) - .then(() => toast.success("Request ID copied")) - .catch(() => toast.error("Failed to copy")); - }} + onClick={() => copyRequestId(displayLog.id)} > {displayLog.id} @@ -192,7 +190,7 @@ export function LogDetailSheet({ log, open, onOpenChange, handleDelete, onNaviga - copyRequestBody(displayLog)} data-testid="logdetails-copy-request-body-button"> + copyRequestBody(displayLog, copyBody)} data-testid="logdetails-copy-request-body-button"> Copy request body @@ -898,7 +896,7 @@ const normalizeObjectForCopy = (object: string | undefined): string => { return mapping[normalized] ?? normalized; }; -const copyRequestBody = async (log: LogEntry) => { +const copyRequestBody = async (log: LogEntry, copy: (text: string) => Promise) => { try { // Check if request is for responses, chat, speech, text completion, or embedding (exclude transcriptions) const object = normalizeObjectForCopy(log.object); @@ -1007,14 +1005,7 @@ const copyRequestBody = async (log: LogEntry) => { } const requestBodyJson = JSON.stringify(requestBody, null, 2); - navigator.clipboard - .writeText(requestBodyJson) - .then(() => { - toast.success("Request body copied to clipboard"); - }) - .catch((error) => { - toast.error("Failed to copy request body"); - }); + await copy(requestBodyJson); } catch (error) { toast.error("Failed to copy request body"); } diff --git a/ui/app/workspace/logs/views/collapsibleBox.tsx b/ui/app/workspace/logs/views/collapsibleBox.tsx index dc1a8b41c3..09d8a109ac 100644 --- a/ui/app/workspace/logs/views/collapsibleBox.tsx +++ b/ui/app/workspace/logs/views/collapsibleBox.tsx @@ -1,7 +1,7 @@ import { Button } from "@/components/ui/button"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { ChevronDown, ChevronUp, Copy } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; interface CollapsibleBoxProps { title: string; @@ -15,6 +15,7 @@ export default function CollapsibleBox({ title, children, collapsedHeight = 60, const [isExpanded, setIsExpanded] = useState(false); const [needsExpansion, setNeedsExpansion] = useState(false); const innerContentRef = useRef(null); + const { copy } = useCopyToClipboard(); useEffect(() => { if (!innerContentRef.current) return; @@ -39,15 +40,7 @@ export default function CollapsibleBox({ title, children, collapsedHeight = 60, const handleCopy = () => { if (!onCopy) return; - - navigator.clipboard - .writeText(onCopy()) - .then(() => { - toast.success("Copied to clipboard"); - }) - .catch(() => { - toast.error("Failed to copy"); - }); + copy(onCopy()); }; return ( diff --git a/ui/app/workspace/logs/views/emptyState.tsx b/ui/app/workspace/logs/views/emptyState.tsx index 5ecb632f92..c4027ef4bc 100644 --- a/ui/app/workspace/logs/views/emptyState.tsx +++ b/ui/app/workspace/logs/views/emptyState.tsx @@ -6,9 +6,9 @@ import { CodeEditor } from "@/components/ui/codeEditor"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { getExampleBaseUrl } from "@/lib/utils/port"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { AlertTriangle, Copy } from "lucide-react"; import { useMemo, useState } from "react"; -import { toast } from "sonner"; type Provider = "openai" | "anthropic" | "genai" | "litellm" | "langchain"; type Language = "python" | "typescript"; @@ -42,10 +42,7 @@ interface CodeBlockProps { } function CodeBlock({ code, language, onLanguageChange, showLanguageSelect = false, readonly = true }: CodeBlockProps) { - const copyToClipboard = () => { - navigator.clipboard.writeText(code); - toast.success("Copied to clipboard"); - }; + const { copy: copyToClipboard } = useCopyToClipboard(); return (
@@ -65,7 +62,7 @@ function CodeBlock({ code, language, onLanguageChange, showLanguageSelect = fals )} -
diff --git a/ui/app/workspace/mcp-logs/views/emptyState.tsx b/ui/app/workspace/mcp-logs/views/emptyState.tsx index 45412fb4dd..a0250ad603 100644 --- a/ui/app/workspace/mcp-logs/views/emptyState.tsx +++ b/ui/app/workspace/mcp-logs/views/emptyState.tsx @@ -6,9 +6,9 @@ import { CodeEditor } from "@/components/ui/codeEditor"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { getExampleBaseUrl } from "@/lib/utils/port"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { AlertTriangle, Copy } from "lucide-react"; import { useMemo, useState } from "react"; -import { toast } from "sonner"; type Language = "python" | "typescript"; @@ -41,10 +41,7 @@ interface CodeBlockProps { } function CodeBlock({ code, language, onLanguageChange, showLanguageSelect = false, readonly = true }: CodeBlockProps) { - const copyToClipboard = () => { - navigator.clipboard.writeText(code); - toast.success("Copied to clipboard"); - }; + const { copy: copyToClipboard } = useCopyToClipboard(); return (
@@ -64,7 +61,7 @@ function CodeBlock({ code, language, onLanguageChange, showLanguageSelect = fals )} -
diff --git a/ui/app/workspace/observability/fragments/prometheusFormFragment.tsx b/ui/app/workspace/observability/fragments/prometheusFormFragment.tsx index e5e36aedf2..1492536bd2 100644 --- a/ui/app/workspace/observability/fragments/prometheusFormFragment.tsx +++ b/ui/app/workspace/observability/fragments/prometheusFormFragment.tsx @@ -10,6 +10,7 @@ import { prometheusFormSchema, type PrometheusFormSchema } from "@/lib/types/sch import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import { zodResolver } from "@hookform/resolvers/zod"; import { Switch } from "@/components/ui/switch"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { AlertTriangle, Copy, Eye, EyeOff, Info, Plus, Trash, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm, type Resolver } from "react-hook-form"; @@ -44,7 +45,7 @@ export function PrometheusFormFragment({ const hasPrometheusAccess = useRbac(RbacResource.Observability, RbacOperation.Update); const [showPassword, setShowPassword] = useState(false); const [isSaving, setIsSaving] = useState(false); - const [copied, setCopied] = useState(false); + const { copy, copied } = useCopyToClipboard(); const [showBasicAuth, setShowBasicAuth] = useState(!!(initialConfig?.basic_auth?.username || initialConfig?.basic_auth?.password)); const form = useForm({ @@ -86,9 +87,7 @@ export function PrometheusFormFragment({ const handleCopyEndpoint = () => { if (metricsEndpoint) { - navigator.clipboard.writeText(metricsEndpoint); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + copy(metricsEndpoint); } }; diff --git a/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx b/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx index 94914de067..2ce19b2df5 100644 --- a/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx +++ b/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx @@ -11,6 +11,7 @@ import { Textarea } from "@/components/ui/textarea"; import { getRoutingFields } from "@/lib/config/celFieldsRouting"; import { celOperatorsRouting } from "@/lib/config/celOperatorsRouting"; import { convertRuleGroupToCEL } from "@/lib/utils/celConverterRouting"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { Check, Copy, Loader2 } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { Field, QueryBuilder, RuleGroupType } from "react-querybuilder"; @@ -46,7 +47,7 @@ export function CELRuleBuilder({ }: CELRuleBuilderProps) { const [query, setQuery] = useState(initialQuery || defaultQuery); const [celExpression, setCelExpression] = useState(""); - const [copied, setCopied] = useState(false); + const { copy, copied } = useCopyToClipboard(); const onChangeRef = useRef(onChange); // Keep ref updated so the query effect always invokes the latest callback @@ -69,11 +70,7 @@ export function CELRuleBuilder({ onChangeRef.current?.(expression, query); }, [query]); - const handleCopy = async () => { - await navigator.clipboard.writeText(celExpression); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; + const handleCopy = () => copy(celExpression); // Show loading state if (isLoading) { diff --git a/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx b/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx index 349cf0c5ee..7caa410984 100644 --- a/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx +++ b/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx @@ -22,6 +22,7 @@ import { resetDurationLabels } from "@/lib/constants/governance" import { cn } from "@/lib/utils" import { formatCurrency } from "@/lib/utils/governance" import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib" +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard" import { ChevronLeft, ChevronRight, Copy, Edit, Eye, EyeOff, Plus, Search, Trash2 } from "lucide-react" import { useMemo, useState } from "react" import { toast } from "sonner" @@ -136,10 +137,7 @@ export default function VirtualKeysTable({ return key.substring(0, 8) + "•".repeat(Math.max(0, key.length - 8)); }; - const copyToClipboard = (key: string) => { - navigator.clipboard.writeText(key); - toast.success("Copied to clipboard"); - }; + const { copy: copyToClipboard } = useCopyToClipboard(); const hasActiveFilters = debouncedSearch || customerFilter || teamFilter; diff --git a/ui/components/ui/input.tsx b/ui/components/ui/input.tsx index 40d5b0946a..37cba85dd9 100644 --- a/ui/components/ui/input.tsx +++ b/ui/components/ui/input.tsx @@ -2,9 +2,9 @@ import * as React from "react"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { cn } from "@/lib/utils"; import { CopyIcon } from "lucide-react"; -import { toast } from "sonner"; import { Button } from "./button"; export interface InputProps extends React.InputHTMLAttributes { @@ -14,6 +14,8 @@ export interface InputProps extends React.InputHTMLAttributes export const Input = React.forwardRef( ({ className, type, showCopyButton = false, inputClassName, ...props }, ref) => { + const { copy } = useCopyToClipboard(); + if (showCopyButton) { return (
( variant="ghost" size="icon" onClick={() => { - if (typeof props.value === "string") { - navigator.clipboard.writeText(props.value as string); - } else { - navigator.clipboard.writeText(JSON.stringify(props.value)); - } - toast.success("Copied to clipboard"); + const text = typeof props.value === "string" ? props.value : JSON.stringify(props.value); + copy(text); }} > diff --git a/ui/hooks/useCopyToClipboard.ts b/ui/hooks/useCopyToClipboard.ts new file mode 100644 index 0000000000..ee22532f3f --- /dev/null +++ b/ui/hooks/useCopyToClipboard.ts @@ -0,0 +1,34 @@ +"use client"; + +import { useCallback, useRef, useState } from "react"; +import { toast } from "sonner"; + +interface UseCopyToClipboardOptions { + successMessage?: string; + errorMessage?: string; + resetDelay?: number; +} + +export function useCopyToClipboard(options: UseCopyToClipboardOptions = {}) { + const { successMessage = "Copied to clipboard", errorMessage = "Failed to copy", resetDelay = 2000 } = options; + const [copied, setCopied] = useState(false); + const timeoutRef = useRef>(undefined); + + const copy = useCallback( + async (text: string) => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + toast.success(successMessage); + + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setCopied(false), resetDelay); + } catch { + toast.error(errorMessage); + } + }, + [successMessage, errorMessage, resetDelay], + ); + + return { copy, copied }; +}