Add Copy-on-Click for IDs (#12615)

* added the mcp_connect copy button to key_info_view

* user id copy button

* copy button on session view

* model id copy

* more copy buttons

* icon size
This commit is contained in:
tanjiro 2025-07-16 03:25:57 +09:00 committed by GitHub
parent d2fe8894c8
commit 720b94fd2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 272 additions and 36 deletions

View file

@ -26,6 +26,8 @@ import PiiConfiguration from "./pii_configuration"
import GuardrailProviderFields from "./guardrail_provider_fields"
import GuardrailOptionalParams from "./guardrail_optional_params"
import { ArrowLeftIcon } from "@heroicons/react/outline"
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"
import { CheckIcon, CopyIcon } from "lucide-react"
export interface GuardrailInfoProps {
guardrailId: string
@ -67,7 +69,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
}>
supported_modes: string[]
} | null>(null)
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({})
const fetchGuardrailInfo = async () => {
try {
setLoading(true)
@ -316,6 +318,16 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
// Format the provider display name and logo
const { logo, displayName } = getGuardrailLogoAndName(guardrailData.litellm_params?.guardrail || "")
const copyToClipboard = async (text: string | null | undefined, key: string) => {
const success = await utilCopyToClipboard(text)
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }))
setTimeout(() => {
setCopiedStates((prev) => ({ ...prev, [key]: false }))
}, 2000)
}
}
return (
<div className="p-4">
<div>
@ -323,7 +335,21 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
Back to Guardrails
</TremorButton>
<Title>{guardrailData.guardrail_name || "Unnamed Guardrail"}</Title>
<Text className="text-gray-500 font-mono">{guardrailData.guardrail_id}</Text>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">{guardrailData.guardrail_id}</Text>
<Button
type="text"
size="small"
icon={copiedStates["guardrail-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(guardrailData.guardrail_id, "guardrail-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["guardrail-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
</div>
</div>
<TabGroup>

View file

@ -18,14 +18,15 @@ import {
import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline";
import { keyDeleteCall, keyUpdateCall } from "./networking";
import { KeyResponse } from "./key_team_helpers/key_list";
import { Form, Input, InputNumber, message, Select, Tooltip } from "antd";
import { Form, Input, InputNumber, message, Select, Tooltip, Button as AntdButton } from "antd";
import { KeyEditView } from "./key_edit_view";
import { RegenerateKeyModal } from "./regenerate_key_modal";
import { rolesWithWriteAccess } from '../utils/roles';
import ObjectPermissionsView from "./object_permissions_view";
import LoggingSettingsView from "./logging_settings_view";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { copyToClipboard as utilCopyToClipboard, formatNumberWithCommas } from "@/utils/dataUtils";
import { extractLoggingSettings, formatMetadataForDisplay } from "./key_info_utils";
import { CopyIcon, CheckIcon } from "lucide-react";
interface KeyInfoViewProps {
keyId: string;
@ -45,6 +46,7 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
const [form] = Form.useForm();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false);
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
if (!keyData) {
return (
@ -152,6 +154,16 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
}
};
const copyToClipboard = async (text: string, key: string) => {
const success = await utilCopyToClipboard(text);
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }));
setTimeout(() => {
setCopiedStates((prev) => ({ ...prev, [key]: false }));
}, 2000);
}
};
return (
<div className="w-full h-screen p-4">
<div className="flex justify-between items-center mb-6">
@ -165,7 +177,21 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
Back to Keys
</Button>
<Title>{keyData.key_alias || "API Key"}</Title>
<Text className="text-gray-500 font-mono">{keyData.token}</Text>
<div className="flex items-center cursor-pointer"
>
<Text className="text-gray-500 font-mono">{keyData.token}</Text>
<AntdButton
type="text"
size="small"
icon={copiedStates["key-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(keyData.token, "key-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["key-id"]
? 'text-green-600 bg-green-50 border-green-200'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
/>
</div>
</div>
{userRole && rolesWithWriteAccess.includes(userRole) && (
<div className="flex gap-2">

View file

@ -35,6 +35,7 @@ import {
Zap
} from "lucide-react";
import { getProxyBaseUrl } from "../networking";
import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
const { Title, Text } = Typography;
const { Panel } = Collapse;
@ -153,15 +154,12 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
const [currentServer] = useState("Zapier_MCP"); // This should match the current server being viewed
const copyToClipboard = async (text: string, key: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedStates(prev => ({ ...prev, [key]: true }));
message.success('Copied to clipboard');
const success = await utilCopyToClipboard(text);
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }));
setTimeout(() => {
setCopiedStates(prev => ({ ...prev, [key]: false }));
setCopiedStates((prev) => ({ ...prev, [key]: false }));
}, 2000);
} catch (err) {
message.error('Failed to copy to clipboard');
}
};
@ -198,7 +196,7 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
<Button
type="text"
size="small"
icon={copiedStates[copyKey] ? <CheckIcon size={14} /> : <CopyIcon size={14} />}
icon={copiedStates[copyKey] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(code, copyKey)}
className={`absolute top-2 right-2 z-10 transition-all duration-200 ${
copiedStates[copyKey]

View file

@ -8,6 +8,9 @@ import { MCPToolsViewer } from "."
import MCPServerEdit from "./mcp_server_edit"
import MCPServerCostDisplay from "./mcp_server_cost_display"
import { getMaskedAndFullUrl } from "./utils"
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"
import { CheckIcon, CopyIcon } from "lucide-react"
import { Button as AntdButton } from "antd"
interface MCPServerViewProps {
mcpServer: MCPServer
@ -32,7 +35,7 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
}) => {
const [editing, setEditing] = useState(isEditing)
const [showFullUrl, setShowFullUrl] = useState(false)
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({})
const handleSuccess = (updated: MCPServer) => {
setEditing(false)
onBack()
@ -45,6 +48,16 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
return showFull ? url : maskedUrl
}
const copyToClipboard = async (text: string | null | undefined, key: string) => {
const success = await utilCopyToClipboard(text)
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }))
setTimeout(() => {
setCopiedStates((prev) => ({ ...prev, [key]: false }))
}, 2000)
}
}
return (
<div className="p-4 max-w-full">
<div className="flex justify-between items-center mb-6">
@ -53,7 +66,20 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
Back to All Servers
</Button>
<Title>{mcpServer.alias}</Title>
<Text className="text-gray-500 font-mono">{mcpServer.server_id}</Text>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">{mcpServer.server_id}</Text>
<AntdButton
type="text"
size="small"
icon={copiedStates["mcp-server-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(mcpServer.server_id, "mcp-server-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["mcp-server-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
</div>
</div>
</div>

View file

@ -33,6 +33,8 @@ import { getDisplayModelName } from "./view_model/model_name_display";
import AddCredentialsModal from "./model_add/add_credentials_tab";
import ReuseCredentialsModal from "./model_add/reuse_credentials";
import CacheControlSettings from "./add_model/cache_control_settings";
import { CheckIcon, CopyIcon } from "lucide-react";
import { copyToClipboard as utilCopyToClipboard } from "../utils/dataUtils";
interface ModelInfoViewProps {
modelId: string;
@ -71,6 +73,7 @@ export default function ModelInfoView({
const [existingCredential, setExistingCredential] =
useState<CredentialItem | null>(null);
const [showCacheControl, setShowCacheControl] = useState(false);
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
const canEditModel =
userRole === "Admin" || modelData.model_info.created_by === userID;
@ -258,6 +261,16 @@ export default function ModelInfoView({
}
};
const copyToClipboard = async (text: string, key: string) => {
const success = await utilCopyToClipboard(text);
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }));
setTimeout(() => {
setCopiedStates((prev) => ({ ...prev, [key]: false }));
}, 2000);
}
};
return (
<div className="p-4">
<div className="flex justify-between items-center mb-6">
@ -271,9 +284,22 @@ export default function ModelInfoView({
Back to Models
</TremorButton>
<Title>Public Model Name: {getDisplayModelName(modelData)}</Title>
<Text className="text-gray-500 font-mono">
{modelData.model_info.id}
</Text>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">
{modelData.model_info.id}
</Text>
<Button
type="text"
size="small"
icon={copiedStates["model-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(modelData.model_info.id, "model-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["model-id"]
? 'text-green-600 bg-green-50 border-green-200'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
/>
</div>
</div>
<div className="flex gap-2">
{isAdmin && (

View file

@ -39,7 +39,8 @@ import MemberModal from "../team/edit_membership"
import ObjectPermissionsView from "../object_permissions_view"
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector"
import MCPServerSelector from "../mcp_server_management/MCPServerSelector"
import { formatNumberWithCommas } from "@/utils/dataUtils"
import { copyToClipboard as utilCopyToClipboard, formatNumberWithCommas } from "@/utils/dataUtils"
import { CheckIcon, CopyIcon } from "lucide-react"
interface OrganizationInfoProps {
organizationId: string
@ -67,7 +68,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false)
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false)
const [selectedEditMember, setSelectedEditMember] = useState<Member | null>(null)
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({})
const canEditOrg = is_org_admin || is_proxy_admin
const fetchOrgInfo = async () => {
@ -204,6 +205,16 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
return <div className="p-4">Organization not found</div>
}
const copyToClipboard = async (text: string | null | undefined, key: string) => {
const success = await utilCopyToClipboard(text)
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }))
setTimeout(() => {
setCopiedStates((prev) => ({ ...prev, [key]: false }))
}, 2000)
}
}
return (
<div className="w-full h-screen p-4 bg-white">
<div className="flex justify-between items-center mb-6">
@ -212,7 +223,20 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
Back to Organizations
</TremorButton>
<Title>{orgData.organization_alias}</Title>
<Text className="text-gray-500 font-mono">{orgData.organization_id}</Text>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">{orgData.organization_id}</Text>
<Button
type="text"
size="small"
icon={copiedStates["org-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(orgData.organization_id, "org-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["org-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
</div>
</div>
</div>

View file

@ -51,6 +51,8 @@ import { formatNumberWithCommas } from "@/utils/dataUtils";
import EditLoggingSettings from "./EditLoggingSettings";
import LoggingSettingsView from "../logging_settings_view";
import { fetchMCPAccessGroups } from "../networking";
import { CheckIcon, CopyIcon } from "lucide-react";
import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils"
export interface TeamMembership {
user_id: string;
@ -143,6 +145,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
const [isEditing, setIsEditing] = useState(false);
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({})
console.log("userModels in team info", userModels);
@ -361,6 +364,16 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
const { team_info: info } = teamData;
const copyToClipboard = async (text: string, key: string) => {
const success = await utilCopyToClipboard(text)
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }))
setTimeout(() => {
setCopiedStates((prev) => ({ ...prev, [key]: false }))
}, 2000)
}
}
return (
<div className="p-4">
<div className="flex justify-between items-center mb-6">
@ -374,7 +387,20 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
Back to Teams
</TremorButton>
<Title>{info.team_alias}</Title>
<Text className="text-gray-500 font-mono">{info.team_id}</Text>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">{info.team_id}</Text>
<Button
type="text"
size="small"
icon={copiedStates["team-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(info.team_id, "team-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["team-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
</div>
</div>
</div>

View file

@ -6,6 +6,9 @@ import { Card, Title, Text, Metric, AreaChart, Button as TremorButton } from "@t
import { RequestViewer } from "./index"
import { formatNumberWithCommas } from "@/utils/dataUtils"
import { ArrowLeftIcon } from "@heroicons/react/outline"
import { Button } from "antd"
import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils"
import { CheckIcon, CopyIcon } from "lucide-react"
interface SessionViewProps {
sessionId: string
@ -16,6 +19,8 @@ interface SessionViewProps {
export const SessionView: React.FC<SessionViewProps> = ({ sessionId, logs, onBack }) => {
// Track which log row is expanded
const [expandedRequestId, setExpandedRequestId] = useState<string | null>(null)
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
// Calculate session metrics
const totalCost = logs.reduce((sum, log) => sum + (log.spend || 0), 0)
const totalTokens = logs.reduce((sum, log) => sum + (log.total_tokens || 0), 0)
@ -31,6 +36,16 @@ export const SessionView: React.FC<SessionViewProps> = ({ sessionId, logs, onBac
cost: log.spend || 0,
}))
const copyToClipboard = async (text: string, key: string) => {
const success = await utilCopyToClipboard(text);
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }));
setTimeout(() => {
setCopiedStates((prev) => ({ ...prev, [key]: false }));
}, 2000);
}
};
return (
<div className="space-y-6">
{/* Header with back button */}
@ -41,7 +56,20 @@ export const SessionView: React.FC<SessionViewProps> = ({ sessionId, logs, onBac
<div className="mt-4">
<h1 className="text-2xl font-semibold text-gray-900">Session Details</h1>
<div className="space-y-2">
<p className="text-sm text-gray-500 font-mono">{sessionId}</p>
<div className="flex items-center cursor-pointer">
<p className="text-sm text-gray-500 font-mono">{sessionId}</p>
<Button
type="text"
size="small"
icon={copiedStates["session-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(sessionId, "session-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["session-id"]
? 'text-green-600 bg-green-50 border-green-200'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
/>
</div>
<a
href="https://docs.litellm.ai/docs/proxy/ui_logs_sessions"
target="_blank"

View file

@ -9,11 +9,12 @@ import {
invitationCreateCall,
getProxyBaseUrl,
} from "../networking"
import { message } from "antd"
import { message, Button as AntdButton } from "antd"
import { rolesWithWriteAccess } from "../../utils/roles"
import { UserEditView } from "../user_edit_view"
import OnboardingModal, { InvitationLink } from "../onboarding_link"
import { formatNumberWithCommas } from "@/utils/dataUtils"
import { formatNumberWithCommas, copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"
import { CopyIcon, CheckIcon } from "lucide-react";
interface UserInfoViewProps {
userId: string
@ -53,15 +54,16 @@ export default function UserInfoView({
initialTab = 0,
startInEditMode = false,
}: UserInfoViewProps) {
const [userData, setUserData] = useState<UserInfo | null>(null)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [isEditing, setIsEditing] = useState(startInEditMode)
const [userModels, setUserModels] = useState<string[]>([])
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false)
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null)
const [baseUrl, setBaseUrl] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState(initialTab)
const [userData, setUserData] = useState<UserInfo | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isEditing, setIsEditing] = useState(startInEditMode);
const [userModels, setUserModels] = useState<string[]>([]);
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false);
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null);
const [baseUrl, setBaseUrl] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState(initialTab);
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
React.useEffect(() => {
setBaseUrl(getProxyBaseUrl())
@ -168,6 +170,16 @@ export default function UserInfoView({
)
}
const copyToClipboard = async (text: string, key: string) => {
const success = await utilCopyToClipboard(text);
if (success) {
setCopiedStates((prev) => ({ ...prev, [key]: true }));
setTimeout(() => {
setCopiedStates((prev) => ({ ...prev, [key]: false }));
}, 2000);
}
};
return (
<div className="p-4">
<div className="flex justify-between items-center mb-6">
@ -176,7 +188,20 @@ export default function UserInfoView({
Back to Users
</Button>
<Title>{userData.user_info?.user_email || "User"}</Title>
<Text className="text-gray-500 font-mono">{userData.user_id}</Text>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">{userData.user_id}</Text>
<AntdButton
type="text"
size="small"
icon={copiedStates["user-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(userData.user_id, "user-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["user-id"]
? 'text-green-600 bg-green-50 border-green-200'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
/>
</div>
</div>
{userRole && rolesWithWriteAccess.includes(userRole) && (
<div className="flex items-center space-x-2">
@ -307,7 +332,20 @@ export default function UserInfoView({
<div className="space-y-4">
<div>
<Text className="font-medium">User ID</Text>
<Text className="font-mono">{userData.user_id}</Text>
<div className="flex items-center cursor-pointer">
<Text className="font-mono">{userData.user_id}</Text>
<AntdButton
type="text"
size="small"
icon={copiedStates["user-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(userData.user_id, "user-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["user-id"]
? 'text-green-600 bg-green-50 border-green-200'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
/>
</div>
</div>
<div>

View file

@ -1,3 +1,5 @@
import { message } from "antd";
export function updateExistingKeys<Source extends Object>(
target: Source,
source: Object
@ -21,4 +23,20 @@ export const formatNumberWithCommas = (value: number | null | undefined, decimal
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
};
export const copyToClipboard = async (
text: string | null | undefined,
messageText: string = "Copied to clipboard"
): Promise<boolean> => {
if (!text) return false;
try {
await navigator.clipboard.writeText(text);
message.success(messageText);
return true;
} catch (err) {
message.error("Failed to copy to clipboard");
console.error("Failed to copy: ", err);
return false;
}
};