mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
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:
parent
d2fe8894c8
commit
720b94fd2b
10 changed files with 272 additions and 36 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue