Spaces:
Sleeping
Sleeping
import React, { useState } from "react"; | |
import { | |
Card, | |
Text, | |
Button, | |
Grid, | |
Col, | |
Tab, | |
TabList, | |
TabGroup, | |
TabPanel, | |
TabPanels, | |
Title, | |
Badge, | |
TextInput, | |
Select as TremorSelect | |
} from "@tremor/react"; | |
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 } from "antd"; | |
import { KeyEditView } from "./key_edit_view"; | |
import { RegenerateKeyModal } from "./regenerate_key_modal"; | |
import { rolesWithWriteAccess } from '../utils/roles'; | |
interface KeyInfoViewProps { | |
keyId: string; | |
onClose: () => void; | |
keyData: KeyResponse | undefined; | |
onKeyDataUpdate?: (data: Partial<KeyResponse>) => void; | |
onDelete?: () => void; | |
accessToken: string | null; | |
userID: string | null; | |
userRole: string | null; | |
teams: any[] | null; | |
} | |
export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams, onKeyDataUpdate, onDelete }: KeyInfoViewProps) { | |
const [isEditing, setIsEditing] = useState(false); | |
const [form] = Form.useForm(); | |
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); | |
const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false); | |
if (!keyData) { | |
return ( | |
<div className="p-4"> | |
<Button | |
icon={ArrowLeftIcon} | |
variant="light" | |
onClick={onClose} | |
className="mb-4" | |
> | |
Back to Keys | |
</Button> | |
<Text>Key not found</Text> | |
</div> | |
); | |
} | |
const handleKeyUpdate = async (formValues: Record<string, any>) => { | |
try { | |
if (!accessToken) return; | |
const currentKey = formValues.token; | |
formValues.key = currentKey; | |
// Convert metadata back to an object if it exists and is a string | |
if (formValues.metadata && typeof formValues.metadata === "string") { | |
try { | |
const parsedMetadata = JSON.parse(formValues.metadata); | |
formValues.metadata = { | |
...parsedMetadata, | |
...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}), | |
}; | |
} catch (error) { | |
console.error("Error parsing metadata JSON:", error); | |
message.error("Invalid metadata JSON"); | |
return; | |
} | |
} else { | |
formValues.metadata = { | |
...(formValues.metadata || {}), | |
...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}), | |
}; | |
} | |
// Convert budget_duration to API format | |
if (formValues.budget_duration) { | |
const durationMap: Record<string, string> = { | |
daily: "24h", | |
weekly: "7d", | |
monthly: "30d" | |
}; | |
formValues.budget_duration = durationMap[formValues.budget_duration]; | |
} | |
const newKeyValues = await keyUpdateCall(accessToken, formValues); | |
if (onKeyDataUpdate) { | |
onKeyDataUpdate(newKeyValues) | |
} | |
message.success("Key updated successfully"); | |
setIsEditing(false); | |
// Refresh key data here if needed | |
} catch (error) { | |
message.error("Failed to update key"); | |
console.error("Error updating key:", error); | |
} | |
}; | |
const handleDelete = async () => { | |
try { | |
if (!accessToken) return; | |
await keyDeleteCall(accessToken as string, keyData.token); | |
message.success("Key deleted successfully"); | |
if (onDelete) { | |
onDelete() | |
} | |
onClose(); | |
} catch (error) { | |
console.error("Error deleting the key:", error); | |
message.error("Failed to delete key"); | |
} | |
}; | |
return ( | |
<div className="p-4"> | |
<div className="flex justify-between items-center mb-6"> | |
<div> | |
<Button | |
icon={ArrowLeftIcon} | |
variant="light" | |
onClick={onClose} | |
className="mb-4" | |
> | |
Back to Keys | |
</Button> | |
<Title>{keyData.key_alias || "API Key"}</Title> | |
<Text className="text-gray-500 font-mono">{keyData.token}</Text> | |
</div> | |
{userRole && rolesWithWriteAccess.includes(userRole) && ( | |
<div className="flex gap-2"> | |
<Button | |
icon={RefreshIcon} | |
variant="secondary" | |
onClick={() => setIsRegenerateModalOpen(true)} | |
className="flex items-center" | |
> | |
Regenerate Key | |
</Button> | |
<Button | |
icon={TrashIcon} | |
variant="secondary" | |
onClick={() => setIsDeleteModalOpen(true)} | |
className="flex items-center" | |
> | |
Delete Key | |
</Button> | |
</div> | |
)} | |
</div> | |
{/* Add RegenerateKeyModal */} | |
<RegenerateKeyModal | |
selectedToken={keyData} | |
visible={isRegenerateModalOpen} | |
onClose={() => setIsRegenerateModalOpen(false)} | |
accessToken={accessToken} | |
/> | |
{/* Delete Confirmation Modal */} | |
{isDeleteModalOpen && ( | |
<div className="fixed z-10 inset-0 overflow-y-auto"> | |
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | |
<div className="fixed inset-0 transition-opacity" aria-hidden="true"> | |
<div className="absolute inset-0 bg-gray-500 opacity-75"></div> | |
</div> | |
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> | |
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> | |
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | |
<div className="sm:flex sm:items-start"> | |
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> | |
<h3 className="text-lg leading-6 font-medium text-gray-900"> | |
Delete Key | |
</h3> | |
<div className="mt-2"> | |
<p className="text-sm text-gray-500"> | |
Are you sure you want to delete this key? | |
</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> | |
<Button | |
onClick={handleDelete} | |
color="red" | |
className="ml-2" | |
> | |
Delete | |
</Button> | |
<Button onClick={() => setIsDeleteModalOpen(false)}> | |
Cancel | |
</Button> | |
</div> | |
</div> | |
</div> | |
</div> | |
)} | |
<TabGroup> | |
<TabList className="mb-4"> | |
<Tab>Overview</Tab> | |
<Tab>Settings</Tab> | |
</TabList> | |
<TabPanels> | |
{/* Overview Panel */} | |
<TabPanel> | |
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6"> | |
<Card> | |
<Text>Spend</Text> | |
<div className="mt-2"> | |
<Title>${Number(keyData.spend).toFixed(4)}</Title> | |
<Text>of {keyData.max_budget !== null ? `$${keyData.max_budget}` : "Unlimited"}</Text> | |
</div> | |
</Card> | |
<Card> | |
<Text>Rate Limits</Text> | |
<div className="mt-2"> | |
<Text>TPM: {keyData.tpm_limit !== null ? keyData.tpm_limit : "Unlimited"}</Text> | |
<Text>RPM: {keyData.rpm_limit !== null ? keyData.rpm_limit : "Unlimited"}</Text> | |
</div> | |
</Card> | |
<Card> | |
<Text>Models</Text> | |
<div className="mt-2 flex flex-wrap gap-2"> | |
{keyData.models && keyData.models.length > 0 ? ( | |
keyData.models.map((model, index) => ( | |
<Badge key={index} color="red"> | |
{model} | |
</Badge> | |
)) | |
) : ( | |
<Text>No models specified</Text> | |
)} | |
</div> | |
</Card> | |
</Grid> | |
</TabPanel> | |
{/* Settings Panel */} | |
<TabPanel> | |
<Card> | |
<div className="flex justify-between items-center mb-4"> | |
<Title>Key Settings</Title> | |
{!isEditing && userRole && rolesWithWriteAccess.includes(userRole) && ( | |
<Button variant="light" onClick={() => setIsEditing(true)}> | |
Edit Settings | |
</Button> | |
)} | |
</div> | |
{isEditing ? ( | |
<KeyEditView | |
keyData={keyData} | |
onCancel={() => setIsEditing(false)} | |
onSubmit={handleKeyUpdate} | |
teams={teams} | |
accessToken={accessToken} | |
userID={userID} | |
userRole={userRole} | |
/> | |
) : ( | |
<div className="space-y-4"> | |
<div> | |
<Text className="font-medium">Key ID</Text> | |
<Text className="font-mono">{keyData.token}</Text> | |
</div> | |
<div> | |
<Text className="font-medium">Key Alias</Text> | |
<Text>{keyData.key_alias || "Not Set"}</Text> | |
</div> | |
<div> | |
<Text className="font-medium">Secret Key</Text> | |
<Text className="font-mono">{keyData.key_name}</Text> | |
</div> | |
<div> | |
<Text className="font-medium">Team ID</Text> | |
<Text>{keyData.team_id || "Not Set"}</Text> | |
</div> | |
<div> | |
<Text className="font-medium">Organization</Text> | |
<Text>{keyData.organization_id || "Not Set"}</Text> | |
</div> | |
<div> | |
<Text className="font-medium">Created</Text> | |
<Text>{new Date(keyData.created_at).toLocaleString()}</Text> | |
</div> | |
<div> | |
<Text className="font-medium">Expires</Text> | |
<Text>{keyData.expires ? new Date(keyData.expires).toLocaleString() : "Never"}</Text> | |
</div> | |
<div> | |
<Text className="font-medium">Spend</Text> | |
<Text>${Number(keyData.spend).toFixed(4)} USD</Text> | |
</div> | |
<div> | |
<Text className="font-medium">Budget</Text> | |
<Text>{keyData.max_budget !== null ? `$${keyData.max_budget} USD` : "Unlimited"}</Text> | |
</div> | |
<div> | |
<Text className="font-medium">Models</Text> | |
<div className="flex flex-wrap gap-2 mt-1"> | |
{keyData.models && keyData.models.length > 0 ? ( | |
keyData.models.map((model, index) => ( | |
<span | |
key={index} | |
className="px-2 py-1 bg-blue-100 rounded text-xs" | |
> | |
{model} | |
</span> | |
)) | |
) : ( | |
<Text>No models specified</Text> | |
)} | |
</div> | |
</div> | |
<div> | |
<Text className="font-medium">Rate Limits</Text> | |
<Text>TPM: {keyData.tpm_limit !== null ? keyData.tpm_limit : "Unlimited"}</Text> | |
<Text>RPM: {keyData.rpm_limit !== null ? keyData.rpm_limit : "Unlimited"}</Text> | |
<Text>Max Parallel Requests: {keyData.max_parallel_requests !== null ? keyData.max_parallel_requests : "Unlimited"}</Text> | |
<Text>Model TPM Limits: {keyData.metadata?.model_tpm_limit ? JSON.stringify(keyData.metadata.model_tpm_limit) : "Unlimited"}</Text> | |
<Text>Model RPM Limits: {keyData.metadata?.model_rpm_limit ? JSON.stringify(keyData.metadata.model_rpm_limit) : "Unlimited"}</Text> | |
</div> | |
<div> | |
<Text className="font-medium">Metadata</Text> | |
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1"> | |
{JSON.stringify(keyData.metadata, null, 2)} | |
</pre> | |
</div> | |
</div> | |
)} | |
</Card> | |
</TabPanel> | |
</TabPanels> | |
</TabGroup> | |
</div> | |
); | |
} |