diff --git a/ui/litellm-dashboard/src/components/key_info_view.tsx b/ui/litellm-dashboard/src/components/key_info_view.tsx index 1c7826fe856..e76aea61eb8 100644 --- a/ui/litellm-dashboard/src/components/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/key_info_view.tsx @@ -15,11 +15,12 @@ import { TextInput, Select as TremorSelect } from "@tremor/react"; -import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline"; +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"; interface KeyInfoViewProps { keyId: string; @@ -29,12 +30,14 @@ interface KeyInfoViewProps { userID: string | null; userRole: string | null; teams: any[] | null; + premiumUser: boolean; } -export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams }: KeyInfoViewProps) { +export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams, premiumUser }: KeyInfoViewProps) { const [isEditing, setIsEditing] = useState(false); const [form] = Form.useForm(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false); if (!keyData) { return ( @@ -126,16 +129,38 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user {keyData.key_alias || "API Key"} {keyData.token} - +
+ + +
+ {/* Add RegenerateKeyModal */} + setIsRegenerateModalOpen(false)} + accessToken={accessToken} + onSuccess={(newKeyData) => { + // Handle the updated key data here if needed + setIsRegenerateModalOpen(false); + }} + /> + {/* Delete Confirmation Modal */} {isDeleteModalOpen && (
diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx index 0f36c86dc5c..946f4d39509 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx @@ -29,6 +29,7 @@ export interface KeyResponse { metadata: Record; tpm_limit: number; rpm_limit: number; + duration: string; budget_duration: string; budget_reset_at: string; allowed_cache_controls: string[]; diff --git a/ui/litellm-dashboard/src/components/regenerate_key_modal.tsx b/ui/litellm-dashboard/src/components/regenerate_key_modal.tsx new file mode 100644 index 00000000000..463a63dd816 --- /dev/null +++ b/ui/litellm-dashboard/src/components/regenerate_key_modal.tsx @@ -0,0 +1,182 @@ +import React, { useEffect, useState } from "react"; +import { Button, Text, TextInput, Title, Grid, Col } from "@tremor/react"; +import { Modal, Form, InputNumber, message } from "antd"; +import { add } from "date-fns"; +import { regenerateKeyCall } from "./networking"; +import { KeyResponse } from "./key_team_helpers/key_list"; +import { CopyToClipboard } from "react-copy-to-clipboard"; + +interface RegenerateKeyModalProps { + selectedToken: KeyResponse | null; + visible: boolean; + onClose: () => void; + accessToken: string | null; + onSuccess?: (newKeyData: any) => void; +} + +export function RegenerateKeyModal({ + selectedToken, + visible, + onClose, + accessToken, + onSuccess, +}: RegenerateKeyModalProps) { + const [form] = Form.useForm(); + const [regeneratedKey, setRegeneratedKey] = useState(null); + const [regenerateFormData, setRegenerateFormData] = useState(null); + const [newExpiryTime, setNewExpiryTime] = useState(null); + + useEffect(() => { + if (visible && selectedToken) { + form.setFieldsValue({ + key_alias: selectedToken.key_alias, + max_budget: selectedToken.max_budget, + tpm_limit: selectedToken.tpm_limit, + rpm_limit: selectedToken.rpm_limit, + duration: selectedToken.duration || "", + }); + } + }, [visible, selectedToken, form]); + + useEffect(() => { + const calculateNewExpiryTime = (duration: string | undefined) => { + if (!duration) return null; + + try { + const now = new Date(); + let newExpiry: Date; + + if (duration.endsWith("s")) { + newExpiry = add(now, { seconds: parseInt(duration) }); + } else if (duration.endsWith("h")) { + newExpiry = add(now, { hours: parseInt(duration) }); + } else if (duration.endsWith("d")) { + newExpiry = add(now, { days: parseInt(duration) }); + } else { + throw new Error("Invalid duration format"); + } + + return newExpiry.toLocaleString(); + } catch (error) { + return null; + } + }; + + if (regenerateFormData?.duration) { + setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration)); + } else { + setNewExpiryTime(null); + } + }, [regenerateFormData?.duration]); + + const handleRegenerateKey = async () => { + + if (!selectedToken || !accessToken) return; + + try { + const formValues = await form.validateFields(); + const response = await regenerateKeyCall(accessToken, selectedToken.token, formValues); + setRegeneratedKey(response.key); + if (onSuccess) { + onSuccess({ ...selectedToken, key_name: response.key_name, ...formValues }); + } + message.success("API Key regenerated successfully"); + } catch (error) { + console.error("Error regenerating key:", error); + message.error("Failed to regenerate API Key"); + } + }; + + const handleClose = () => { + setRegeneratedKey(null); + form.resetFields(); + onClose(); + }; + + return ( + + Close + , + ] : [ + , + , + ]} + > + {regeneratedKey ? ( + + Regenerated Key + +

+ Please replace your old key with the new key generated. For + security reasons, you will not be able to view it again{" "} + through your LiteLLM account. If you lose this secret key, you + will need to generate a new one. +

+ + + Key Alias: +
+
+                {selectedToken?.key_alias || "No alias set"}
+              
+
+ New API Key: +
+
{regeneratedKey}
+
+ message.success("API Key copied to clipboard")} + > + + + +
+ ) : ( +
{ + if ("duration" in changedValues) { + setRegenerateFormData((prev: { duration?: string }) => ({ ...prev, duration: changedValues.duration })); + } + }} + > + + + + + + + + + + + + + + + +
+ Current expiry: {selectedToken?.expires ? new Date(selectedToken.expires).toLocaleString() : "Never"} +
+ {newExpiryTime && ( +
+ New expiry: {newExpiryTime} +
+ )} +
+ ) + } +
+ ); +} \ No newline at end of file