diff --git a/ui/litellm-dashboard/src/components/guardrails/guardrail_info.tsx b/ui/litellm-dashboard/src/components/guardrails/guardrail_info.tsx index 36de2166a36..ec02c412dcd 100644 --- a/ui/litellm-dashboard/src/components/guardrails/guardrail_info.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/guardrail_info.tsx @@ -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 = ({ guardrailId, onClose, }> supported_modes: string[] } | null>(null) - + const [copiedStates, setCopiedStates] = useState>({}) const fetchGuardrailInfo = async () => { try { setLoading(true) @@ -316,6 +318,16 @@ const GuardrailInfoView: React.FC = ({ 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 (
@@ -323,7 +335,21 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, Back to Guardrails {guardrailData.guardrail_name || "Unnamed Guardrail"} - {guardrailData.guardrail_id} +
+ {guardrailData.guardrail_id} + +
diff --git a/ui/litellm-dashboard/src/components/key_info_view.tsx b/ui/litellm-dashboard/src/components/key_info_view.tsx index d0169fcaf2f..fa0e1818d1b 100644 --- a/ui/litellm-dashboard/src/components/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/key_info_view.tsx @@ -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>({}); 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 (
@@ -165,7 +177,21 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user Back to Keys {keyData.key_alias || "API Key"} - {keyData.token} +
+ {keyData.token} + : } + 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' + }`} + /> +
{userRole && rolesWithWriteAccess.includes(userRole) && (
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx index 833d436dd62..9298127822d 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx @@ -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 = ({ 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 = ({ currentServerAccessGroups = [] {mcpServer.alias} - {mcpServer.server_id} +
+ {mcpServer.server_id} + : } + 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" + }`} + /> +
diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index cf82d7df0de..da15c3a3d40 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -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(null); const [showCacheControl, setShowCacheControl] = useState(false); + const [copiedStates, setCopiedStates] = useState>({}); 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 (
@@ -271,9 +284,22 @@ export default function ModelInfoView({ Back to Models Public Model Name: {getDisplayModelName(modelData)} - - {modelData.model_info.id} - +
+ + {modelData.model_info.id} + +
{isAdmin && ( diff --git a/ui/litellm-dashboard/src/components/organization/organization_view.tsx b/ui/litellm-dashboard/src/components/organization/organization_view.tsx index d9ce9005ae2..c0eb59ff316 100644 --- a/ui/litellm-dashboard/src/components/organization/organization_view.tsx +++ b/ui/litellm-dashboard/src/components/organization/organization_view.tsx @@ -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 = ({ const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false) const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false) const [selectedEditMember, setSelectedEditMember] = useState(null) - + const [copiedStates, setCopiedStates] = useState>({}) const canEditOrg = is_org_admin || is_proxy_admin const fetchOrgInfo = async () => { @@ -204,6 +205,16 @@ const OrganizationInfoView: React.FC = ({ return
Organization not found
} + 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 (
@@ -212,7 +223,20 @@ const OrganizationInfoView: React.FC = ({ Back to Organizations {orgData.organization_alias} - {orgData.organization_id} +
+ {orgData.organization_id} +
diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx index c2b4a402f75..fd53761e635 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.tsx @@ -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 = ({ const [isEditing, setIsEditing] = useState(false); const [mcpAccessGroups, setMcpAccessGroups] = useState([]); const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); + const [copiedStates, setCopiedStates] = useState>({}) console.log("userModels in team info", userModels); @@ -361,6 +364,16 @@ const TeamInfoView: React.FC = ({ 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 (
@@ -374,7 +387,20 @@ const TeamInfoView: React.FC = ({ Back to Teams {info.team_alias} - {info.team_id} +
+ {info.team_id} +
diff --git a/ui/litellm-dashboard/src/components/view_logs/SessionView.tsx b/ui/litellm-dashboard/src/components/view_logs/SessionView.tsx index bb655ba0e8f..efbae1bf92a 100644 --- a/ui/litellm-dashboard/src/components/view_logs/SessionView.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/SessionView.tsx @@ -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 = ({ sessionId, logs, onBack }) => { // Track which log row is expanded const [expandedRequestId, setExpandedRequestId] = useState(null) + const [copiedStates, setCopiedStates] = useState>({}); + // 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 = ({ 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 (
{/* Header with back button */} @@ -41,7 +56,20 @@ export const SessionView: React.FC = ({ sessionId, logs, onBac

Session Details

-

{sessionId}

+
+

{sessionId}

+
(null) - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) - const [isLoading, setIsLoading] = useState(true) - const [isEditing, setIsEditing] = useState(startInEditMode) - const [userModels, setUserModels] = useState([]) - const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false) - const [invitationLinkData, setInvitationLinkData] = useState(null) - const [baseUrl, setBaseUrl] = useState(null) - const [activeTab, setActiveTab] = useState(initialTab) + const [userData, setUserData] = useState(null); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isEditing, setIsEditing] = useState(startInEditMode); + const [userModels, setUserModels] = useState([]); + const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false); + const [invitationLinkData, setInvitationLinkData] = useState(null); + const [baseUrl, setBaseUrl] = useState(null); + const [activeTab, setActiveTab] = useState(initialTab); + const [copiedStates, setCopiedStates] = useState>({}); 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 (
@@ -176,7 +188,20 @@ export default function UserInfoView({ Back to Users {userData.user_info?.user_email || "User"} - {userData.user_id} +
+ {userData.user_id} + : } + 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' + }`} + /> +
{userRole && rolesWithWriteAccess.includes(userRole) && (
@@ -307,7 +332,20 @@ export default function UserInfoView({
User ID - {userData.user_id} +
+ {userData.user_id} + : } + 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' + }`} + /> +
diff --git a/ui/litellm-dashboard/src/utils/dataUtils.ts b/ui/litellm-dashboard/src/utils/dataUtils.ts index b7abef9d7bb..0aeefbadc03 100644 --- a/ui/litellm-dashboard/src/utils/dataUtils.ts +++ b/ui/litellm-dashboard/src/utils/dataUtils.ts @@ -1,3 +1,5 @@ +import { message } from "antd"; + export function updateExistingKeys( 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 => { + 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; + } }; \ No newline at end of file