From 03e420f91a7450699d4fceefe82bc41352bc4ecc Mon Sep 17 00:00:00 2001 From: tanjiro <56165694+NANDINI-star@users.noreply.github.com> Date: Tue, 22 Jul 2025 23:47:26 +0900 Subject: [PATCH] Improvements on the Regenerate Key Flow (#12788) * improve regeneration ux * toLocaleString * regenerate key twice in a row * fix delete key --- .../src/components/all_keys_table.tsx | 17 +- .../src/components/key_info_view.tsx | 294 +++++++++++++----- .../components/key_team_helpers/key_list.tsx | 1 + .../src/components/regenerate_key_modal.tsx | 115 +++++-- .../src/components/user_dashboard.tsx | 1 + .../src/components/view_key_table.tsx | 41 +-- 6 files changed, 334 insertions(+), 135 deletions(-) diff --git a/ui/litellm-dashboard/src/components/all_keys_table.tsx b/ui/litellm-dashboard/src/components/all_keys_table.tsx index dd5b469d7d..aa2b92a585 100644 --- a/ui/litellm-dashboard/src/components/all_keys_table.tsx +++ b/ui/litellm-dashboard/src/components/all_keys_table.tsx @@ -41,9 +41,9 @@ import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_t import { formatNumberWithCommas } from "@/utils/dataUtils"; interface AllKeysTableProps { - keys: KeyResponse[]; - setKeys: Setter; - isLoading?: boolean; + keys: KeyResponse[] + setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void + isLoading?: boolean pagination: { currentPage: number; totalPages: number; @@ -64,10 +64,11 @@ interface AllKeysTableProps { refresh?: () => void; onSortChange?: (sortBy: string, sortOrder: 'asc' | 'desc') => void; currentSort?: { - sortBy: string; - sortOrder: 'asc' | 'desc'; - }; - premiumUser: boolean; + sortBy: string + sortOrder: "asc" | "desc" + } + premiumUser: boolean + setAccessToken?: (token: string) => void } // Define columns similar to our logs table @@ -143,6 +144,7 @@ export function AllKeysTable({ onSortChange, currentSort, premiumUser, + setAccessToken, }: AllKeysTableProps) { const [selectedKeyId, setSelectedKeyId] = useState(null); const [userList, setUserList] = useState([]); @@ -625,6 +627,7 @@ export function AllKeysTable({ userRole={userRole} teams={allTeams} premiumUser={premiumUser} + setAccessToken={setAccessToken} /> ) : (
diff --git a/ui/litellm-dashboard/src/components/key_info_view.tsx b/ui/litellm-dashboard/src/components/key_info_view.tsx index ad60c07835..438e496357 100644 --- a/ui/litellm-dashboard/src/components/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/key_info_view.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Card, Text, @@ -30,26 +30,62 @@ import { CopyIcon, CheckIcon } from "lucide-react"; import { callback_map, mapInternalToDisplayNames, mapDisplayToInternalNames } from "./callback_info_helpers"; interface KeyInfoViewProps { - keyId: string; - onClose: () => void; - keyData: KeyResponse | undefined; - onKeyDataUpdate?: (data: Partial) => void; - onDelete?: () => void; - accessToken: string | null; - userID: string | null; - userRole: string | null; - teams: any[] | null; - premiumUser: boolean; + keyId: string + onClose: () => void + keyData: KeyResponse | undefined + onKeyDataUpdate?: (data: Partial) => void + onDelete?: () => void + accessToken: string | null + userID: string | null + userRole: string | null + teams: any[] | null + premiumUser: boolean + setAccessToken?: (token: string) => void } -export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams, onKeyDataUpdate, onDelete, premiumUser }: KeyInfoViewProps) { - const [isEditing, setIsEditing] = useState(false); - const [form] = Form.useForm(); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false); - const [copiedStates, setCopiedStates] = useState>({}); +export default function KeyInfoView({ + keyId, + onClose, + keyData, + accessToken, + userID, + userRole, + teams, + onKeyDataUpdate, + onDelete, + premiumUser, + setAccessToken, +}: KeyInfoViewProps) { + const [isEditing, setIsEditing] = useState(false) + const [form] = Form.useForm() + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) + const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false) + const [copiedStates, setCopiedStates] = useState>({}) - if (!keyData) { + // Add local state to maintain key data and track regeneration + const [currentKeyData, setCurrentKeyData] = useState(keyData) + const [lastRegeneratedAt, setLastRegeneratedAt] = useState(null) + const [isRecentlyRegenerated, setIsRecentlyRegenerated] = useState(false) + + // Update local state when keyData prop changes (but don't reset to undefined) + useEffect(() => { + if (keyData) { + setCurrentKeyData(keyData) + } + }, [keyData]) + + // Reset recent regeneration indicator after 5 seconds + useEffect(() => { + if (isRecentlyRegenerated) { + const timer = setTimeout(() => { + setIsRecentlyRegenerated(false) + }, 5000) + return () => clearTimeout(timer) + } + }, [isRecentlyRegenerated]) + + // Use currentKeyData instead of keyData throughout the component + if (!currentKeyData) { return (
- {keyData.key_alias || "API Key"} -
- {keyData.token} + {currentKeyData.key_alias || "API Key"} + +
+
+ Key ID + {currentKeyData.token_id || currentKeyData.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' + onClick={() => copyToClipboard(currentKeyData.token_id || currentKeyData.token, "key-id")} + className={`ml-2 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" }`} />
+ + {/* Add timestamp and regeneration indicator */} +
+ + {currentKeyData.updated_at && currentKeyData.updated_at !== currentKeyData.created_at + ? `Updated: ${formatTimestamp(currentKeyData.updated_at)}` + : `Created: ${formatTimestamp(currentKeyData.created_at)}`} + + + {isRecentlyRegenerated && ( + + Recently Regenerated + + )} + + {lastRegeneratedAt && ( + + Regenerated + + )} +
{userRole && rolesWithWriteAccess.includes(userRole) && (
@@ -241,11 +346,13 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user {/* Add RegenerateKeyModal */} setIsRegenerateModalOpen(false)} accessToken={accessToken} premiumUser={premiumUser} + setAccessToken={setAccessToken} + onKeyUpdate={handleRegenerateKeyUpdate} /> {/* Delete Confirmation Modal */} @@ -303,24 +410,29 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user Spend
- ${formatNumberWithCommas(keyData.spend, 4)} - of {keyData.max_budget !== null ? `$${formatNumberWithCommas(keyData.max_budget)}` : "Unlimited"} + ${formatNumberWithCommas(currentKeyData.spend, 4)} + + of{" "} + {currentKeyData.max_budget !== null + ? `$${formatNumberWithCommas(currentKeyData.max_budget)}` + : "Unlimited"} +
Rate Limits
- TPM: {keyData.tpm_limit !== null ? keyData.tpm_limit : "Unlimited"} - RPM: {keyData.rpm_limit !== null ? keyData.rpm_limit : "Unlimited"} + TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"} + RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"}
Models
- {keyData.models && keyData.models.length > 0 ? ( - keyData.models.map((model, index) => ( + {currentKeyData.models && currentKeyData.models.length > 0 ? ( + currentKeyData.models.map((model, index) => ( {model} @@ -333,17 +445,19 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user @@ -363,7 +477,7 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user {isEditing ? ( setIsEditing(false)} onSubmit={handleKeyUpdate} teams={teams} @@ -375,49 +489,61 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
Key ID - {keyData.token} + {currentKeyData.token_id || currentKeyData.token}
Key Alias - {keyData.key_alias || "Not Set"} + {currentKeyData.key_alias || "Not Set"}
Secret Key - {keyData.key_name} + {currentKeyData.key_name}
Team ID - {keyData.team_id || "Not Set"} + {currentKeyData.team_id || "Not Set"}
Organization - {keyData.organization_id || "Not Set"} + {currentKeyData.organization_id || "Not Set"}
Created - {new Date(keyData.created_at).toLocaleString()} + {formatTimestamp(currentKeyData.created_at)}
+ {lastRegeneratedAt && ( +
+ Last Regenerated +
+ {formatTimestamp(lastRegeneratedAt)} + + Recent + +
+
+ )} +
Expires - {keyData.expires ? new Date(keyData.expires).toLocaleString() : "Never"} + {currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never"}
Spend - ${formatNumberWithCommas(keyData.spend, 4)} USD + ${formatNumberWithCommas(currentKeyData.spend, 4)} USD
Budget - {keyData.max_budget !== null - ? `$${formatNumberWithCommas(keyData.max_budget, 2)}` + {currentKeyData.max_budget !== null + ? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}` : "Unlimited"}
@@ -425,12 +551,9 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
Models
- {keyData.models && keyData.models.length > 0 ? ( - keyData.models.map((model, index) => ( - + {currentKeyData.models && currentKeyData.models.length > 0 ? ( + currentKeyData.models.map((model, index) => ( + {model} )) @@ -442,34 +565,49 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
Rate Limits - TPM: {keyData.tpm_limit !== null ? keyData.tpm_limit : "Unlimited"} - RPM: {keyData.rpm_limit !== null ? keyData.rpm_limit : "Unlimited"} - Max Parallel Requests: {keyData.max_parallel_requests !== null ? keyData.max_parallel_requests : "Unlimited"} - Model TPM Limits: {keyData.metadata?.model_tpm_limit ? JSON.stringify(keyData.metadata.model_tpm_limit) : "Unlimited"} - Model RPM Limits: {keyData.metadata?.model_rpm_limit ? JSON.stringify(keyData.metadata.model_rpm_limit) : "Unlimited"} + TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"} + RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"} + + Max Parallel Requests:{" "} + {currentKeyData.max_parallel_requests !== null + ? currentKeyData.max_parallel_requests + : "Unlimited"} + + + Model TPM Limits:{" "} + {currentKeyData.metadata?.model_tpm_limit + ? JSON.stringify(currentKeyData.metadata.model_tpm_limit) + : "Unlimited"} + + + Model RPM Limits:{" "} + {currentKeyData.metadata?.model_rpm_limit + ? JSON.stringify(currentKeyData.metadata.model_rpm_limit) + : "Unlimited"} +
Metadata
-                      {formatMetadataForDisplay(keyData.metadata)}
+                      {formatMetadataForDisplay(currentKeyData.metadata)}
                     
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 e3677ddb33..9933c4ee5b 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 @@ -18,6 +18,7 @@ export interface Team { export interface KeyResponse { token: string; + token_id: string; key_name: string; key_alias: string; spend: number; diff --git a/ui/litellm-dashboard/src/components/regenerate_key_modal.tsx b/ui/litellm-dashboard/src/components/regenerate_key_modal.tsx index 84a406d2fc..75b59a8a11 100644 --- a/ui/litellm-dashboard/src/components/regenerate_key_modal.tsx +++ b/ui/litellm-dashboard/src/components/regenerate_key_modal.tsx @@ -7,11 +7,13 @@ 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; - premiumUser: boolean; + selectedToken: KeyResponse | null + visible: boolean + onClose: () => void + accessToken: string | null + premiumUser: boolean + setAccessToken?: (token: string) => void + onKeyUpdate?: (updatedKeyData: Partial) => void } export function RegenerateKeyModal({ @@ -20,37 +22,53 @@ export function RegenerateKeyModal({ onClose, accessToken, premiumUser, + setAccessToken, + onKeyUpdate, }: RegenerateKeyModalProps) { - const [form] = Form.useForm(); - const [regeneratedKey, setRegeneratedKey] = useState(null); - const [regenerateFormData, setRegenerateFormData] = useState(null); - const [newExpiryTime, setNewExpiryTime] = useState(null); - const [isRegenerating, setIsRegenerating] = useState(false); + const [form] = Form.useForm() + const [regeneratedKey, setRegeneratedKey] = useState(null) + const [regenerateFormData, setRegenerateFormData] = useState(null) + const [newExpiryTime, setNewExpiryTime] = useState(null) + const [isRegenerating, setIsRegenerating] = useState(false) + + // Track whether this is the user's own authentication key + const [isOwnKey, setIsOwnKey] = useState(false) + + // Keep track of the current valid access token locally + const [currentAccessToken, setCurrentAccessToken] = useState(null) useEffect(() => { - if (visible && selectedToken) { + if (visible && selectedToken && accessToken) { 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 || "", - }); + }) + + // Initialize the current access token + setCurrentAccessToken(accessToken) + + // Check if this is the user's own authentication key by comparing the key values + const isUserOwnKey = selectedToken.key_name === accessToken + setIsOwnKey(isUserOwnKey) } - }, [visible, selectedToken, form]); + }, [visible, selectedToken, form, accessToken]) useEffect(() => { if (!visible) { // Reset states when modal is closed - setRegeneratedKey(null); - setIsRegenerating(false); - form.resetFields(); + setRegeneratedKey(null) + setIsRegenerating(false) + setIsOwnKey(false) + setCurrentAccessToken(null) + form.resetFields() } - }, [visible, form]); + }, [visible, form]) - useEffect(() => { - const calculateNewExpiryTime = (duration: string | undefined) => { - if (!duration) return null; + const calculateNewExpiryTime = (duration: string | undefined): string | null => { + if (!duration) return null try { const now = new Date(); @@ -72,6 +90,7 @@ export function RegenerateKeyModal({ } }; + useEffect(() => { if (regenerateFormData?.duration) { setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration)); } else { @@ -80,14 +99,48 @@ export function RegenerateKeyModal({ }, [regenerateFormData?.duration]); const handleRegenerateKey = async () => { - if (!selectedToken || !accessToken) return; + if (!selectedToken || !currentAccessToken) return setIsRegenerating(true); try { - const formValues = await form.validateFields(); - const response = await regenerateKeyCall(accessToken, selectedToken.token, formValues); - setRegeneratedKey(response.key); - message.success("API Key regenerated successfully"); + const formValues = await form.validateFields() + + // Use the current access token for the API call + const response = await regenerateKeyCall(currentAccessToken, selectedToken.token || selectedToken.token_id, formValues) + setRegeneratedKey(response.key) + message.success("API Key regenerated successfully") + + console.log("Full regenerate response:", response) // Debug log to see what's returned + + // Create updated key data with ALL new values from the response + const updatedKeyData: Partial = { + // Use the new token/key ID from the response (this is what was missing!) + token: response.token || response.key_id || selectedToken.token, // Try different possible field names + key_name: response.key, // This is the new secret key string + max_budget: formValues.max_budget, + tpm_limit: formValues.tpm_limit, + rpm_limit: formValues.rpm_limit, + expires: formValues.duration ? calculateNewExpiryTime(formValues.duration) : selectedToken.expires, + // Include any other fields that might be returned by the API + ...response, // Spread the entire response to capture all updated fields + } + + console.log("Updated key data with new token:", updatedKeyData) // Debug log + + // If user regenerated their own auth key, update both local and global access tokens + if (isOwnKey) { + setCurrentAccessToken(response.key) // Update local token immediately + if (setAccessToken) { + setAccessToken(response.key) // Update global token + } + } + + // Update the parent component with new key data + if (onKeyUpdate) { + onKeyUpdate(updatedKeyData) + } + + setIsRegenerating(false) } catch (error) { console.error("Error regenerating key:", error); message.error("Failed to regenerate API Key"); @@ -96,11 +149,13 @@ export function RegenerateKeyModal({ }; const handleClose = () => { - setRegeneratedKey(null); - setIsRegenerating(false); - form.resetFields(); - onClose(); - }; + setRegeneratedKey(null) + setIsRegenerating(false) + setIsOwnKey(false) + setCurrentAccessToken(null) + form.resetFields() + onClose() + } return ( = ({ setCurrentOrg={setCurrentOrg} organizations={organizations} createClicked={createClicked} + setAccessToken={setAccessToken} /> diff --git a/ui/litellm-dashboard/src/components/view_key_table.tsx b/ui/litellm-dashboard/src/components/view_key_table.tsx index 2faed3a81f..094ae80ae6 100644 --- a/ui/litellm-dashboard/src/components/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/view_key_table.tsx @@ -90,21 +90,22 @@ interface ModelLimitModalProps { // Define the props type interface ViewKeyTableProps { - userID: string; - userRole: string | null; - accessToken: string; - selectedTeam: any | null; + userID: string | null + userRole: string | null + accessToken: string | null + selectedTeam: Team | null setSelectedTeam: React.Dispatch>; - data: any[] | null; + data: KeyResponse[] | null setData: React.Dispatch>; teams: Team[] | null; premiumUser: boolean; currentOrg: Organization | null; organizations: Organization[] | null; setCurrentOrg: React.Dispatch>; - selectedKeyAlias: string | null; - setSelectedKeyAlias: Setter; - createClicked: boolean; + selectedKeyAlias: string | null + setSelectedKeyAlias: Setter + createClicked: boolean + setAccessToken?: (token: string) => void } interface ItemData { @@ -155,7 +156,8 @@ const ViewKeyTable: React.FC = ({ setCurrentOrg, selectedKeyAlias, setSelectedKeyAlias, - createClicked + createClicked, + setAccessToken, }) => { const [isButtonClicked, setIsButtonClicked] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -178,10 +180,10 @@ const ViewKeyTable: React.FC = ({ // Pass filters into the hook so the API call includes these query parameters. const { keys, isLoading, error, pagination, refresh, setKeys } = useKeyList({ - selectedTeam, + selectedTeam: selectedTeam || undefined, currentOrg, selectedKeyAlias, - accessToken, + accessToken: accessToken || "", createClicked, }); @@ -292,7 +294,8 @@ const ViewKeyTable: React.FC = ({ } try { - await keyDeleteCall(accessToken, keyToDelete); + if (!accessToken) return + await keyDeleteCall(accessToken, keyToDelete) // Successfully completed the deletion. Update the state to trigger a rerender. const filteredData = data.filter((item) => item.token !== keyToDelete); setData(filteredData); @@ -345,13 +348,10 @@ const ViewKeyTable: React.FC = ({ } try { - const formValues = await regenerateForm.validateFields(); - const response = await regenerateKeyCall( - accessToken, - selectedToken.token, - formValues - ); - setRegeneratedKey(response.key); + const formValues = await regenerateForm.validateFields() + if (!accessToken) return; + const response = await regenerateKeyCall(accessToken, selectedToken.token || selectedToken.token_id, formValues) + setRegeneratedKey(response.key) // Update the data state with the new key_name if (data) { @@ -375,7 +375,7 @@ const ViewKeyTable: React.FC = ({ return (
- = ({ selectedKeyAlias={selectedKeyAlias} setSelectedKeyAlias={setSelectedKeyAlias} premiumUser={premiumUser} + setAccessToken={setAccessToken} /> {isDeleteModalOpen && (