mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-07 13:13:18 +00:00
Improvements on the Regenerate Key Flow (#12788)
* improve regeneration ux * toLocaleString * regenerate key twice in a row * fix delete key
This commit is contained in:
@@ -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<KeyResponse[]>;
|
||||
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<string | null>(null);
|
||||
const [userList, setUserList] = useState<UserResponse[]>([]);
|
||||
@@ -625,6 +627,7 @@ export function AllKeysTable({
|
||||
userRole={userRole}
|
||||
teams={allTeams}
|
||||
premiumUser={premiumUser}
|
||||
setAccessToken={setAccessToken}
|
||||
/>
|
||||
) : (
|
||||
<div className="border-b py-4 flex-1 overflow-hidden">
|
||||
|
||||
@@ -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<KeyResponse>) => 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<KeyResponse>) => 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<Record<string, boolean>>({});
|
||||
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<Record<string, boolean>>({})
|
||||
|
||||
if (!keyData) {
|
||||
// Add local state to maintain key data and track regeneration
|
||||
const [currentKeyData, setCurrentKeyData] = useState<KeyResponse | undefined>(keyData)
|
||||
const [lastRegeneratedAt, setLastRegeneratedAt] = useState<Date | null>(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 (
|
||||
<div className="p-4">
|
||||
<Button
|
||||
@@ -75,7 +111,7 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
// Handle object_permission updates
|
||||
if (formValues.vector_stores !== undefined) {
|
||||
formValues.object_permission = {
|
||||
...keyData.object_permission,
|
||||
...currentKeyData.object_permission,
|
||||
vector_stores: formValues.vector_stores || []
|
||||
};
|
||||
// Remove vector_stores from the top level as it should be in object_permission
|
||||
@@ -85,7 +121,7 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
if (formValues.mcp_servers_and_groups !== undefined) {
|
||||
const { servers, accessGroups } = formValues.mcp_servers_and_groups || { servers: [], accessGroups: [] };
|
||||
formValues.object_permission = {
|
||||
...keyData.object_permission,
|
||||
...currentKeyData.object_permission,
|
||||
mcp_servers: servers || [],
|
||||
mcp_access_groups: accessGroups || []
|
||||
};
|
||||
@@ -145,7 +181,11 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
formValues.budget_duration = durationMap[formValues.budget_duration];
|
||||
}
|
||||
|
||||
const newKeyValues = await keyUpdateCall(accessToken, formValues);
|
||||
const newKeyValues = await keyUpdateCall(accessToken, formValues)
|
||||
|
||||
// Update local state
|
||||
setCurrentKeyData((prevData) => (prevData ? { ...prevData, ...newKeyValues } : undefined))
|
||||
|
||||
if (onKeyDataUpdate) {
|
||||
onKeyDataUpdate(newKeyValues)
|
||||
}
|
||||
@@ -160,9 +200,9 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
await keyDeleteCall(accessToken as string, keyData.token);
|
||||
message.success("Key deleted successfully");
|
||||
if (!accessToken) return
|
||||
await keyDeleteCall(accessToken as string, currentKeyData.token || currentKeyData.token_id)
|
||||
message.success("Key deleted successfully")
|
||||
if (onDelete) {
|
||||
onDelete()
|
||||
}
|
||||
@@ -178,10 +218,51 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
if (success) {
|
||||
setCopiedStates((prev) => ({ ...prev, [key]: true }));
|
||||
setTimeout(() => {
|
||||
setCopiedStates((prev) => ({ ...prev, [key]: false }));
|
||||
}, 2000);
|
||||
setCopiedStates((prev) => ({ ...prev, [key]: false }))
|
||||
}, 2000)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleRegenerateKeyUpdate = (updatedKeyData: Partial<KeyResponse>) => {
|
||||
// Update local state immediately with ALL the new data
|
||||
setCurrentKeyData((prevData) => {
|
||||
if (!prevData) return undefined
|
||||
const newData = {
|
||||
...prevData,
|
||||
...updatedKeyData, // This should include the new token (key-id)
|
||||
// Update the created_at to show when it was regenerated
|
||||
created_at: new Date().toLocaleString(),
|
||||
}
|
||||
return newData
|
||||
})
|
||||
|
||||
// Track regeneration timestamp
|
||||
setLastRegeneratedAt(new Date())
|
||||
setIsRecentlyRegenerated(true)
|
||||
|
||||
if (onKeyDataUpdate) {
|
||||
onKeyDataUpdate({
|
||||
...updatedKeyData,
|
||||
created_at: new Date().toLocaleString(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Update the formatTimestamp function to use the desired date format
|
||||
const formatTimestamp = (timestamp: string | Date) => {
|
||||
const date = new Date(timestamp)
|
||||
const dateStr = date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
const timeStr = date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
})
|
||||
return `${dateStr} at ${timeStr}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen p-4">
|
||||
@@ -195,22 +276,46 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
>
|
||||
Back to Keys
|
||||
</Button>
|
||||
<Title>{keyData.key_alias || "API Key"}</Title>
|
||||
<div className="flex items-center cursor-pointer"
|
||||
>
|
||||
<Text className="text-gray-500 font-mono">{keyData.token}</Text>
|
||||
<Title>{currentKeyData.key_alias || "API Key"}</Title>
|
||||
|
||||
<div className="flex items-center cursor-pointer mb-2 space-y-6">
|
||||
<div>
|
||||
<Text className="text-xs text-gray-400 uppercase tracking-wide mt-2">Key ID</Text>
|
||||
<Text className="text-gray-500 font-mono text-sm">{currentKeyData.token_id || currentKeyData.token}</Text>
|
||||
</div>
|
||||
<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'
|
||||
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"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Add timestamp and regeneration indicator */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Text className="text-sm text-gray-500">
|
||||
{currentKeyData.updated_at && currentKeyData.updated_at !== currentKeyData.created_at
|
||||
? `Updated: ${formatTimestamp(currentKeyData.updated_at)}`
|
||||
: `Created: ${formatTimestamp(currentKeyData.created_at)}`}
|
||||
</Text>
|
||||
|
||||
{isRecentlyRegenerated && (
|
||||
<Badge color="green" size="xs" className="animate-pulse">
|
||||
Recently Regenerated
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{lastRegeneratedAt && (
|
||||
<Badge color="blue" size="xs">
|
||||
Regenerated
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{userRole && rolesWithWriteAccess.includes(userRole) && (
|
||||
<div className="flex gap-2">
|
||||
@@ -241,11 +346,13 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
|
||||
{/* Add RegenerateKeyModal */}
|
||||
<RegenerateKeyModal
|
||||
selectedToken={keyData}
|
||||
selectedToken={currentKeyData}
|
||||
visible={isRegenerateModalOpen}
|
||||
onClose={() => 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
|
||||
<Card>
|
||||
<Text>Spend</Text>
|
||||
<div className="mt-2">
|
||||
<Title>${formatNumberWithCommas(keyData.spend, 4)}</Title>
|
||||
<Text>of {keyData.max_budget !== null ? `$${formatNumberWithCommas(keyData.max_budget)}` : "Unlimited"}</Text>
|
||||
<Title>${formatNumberWithCommas(currentKeyData.spend, 4)}</Title>
|
||||
<Text>
|
||||
of{" "}
|
||||
{currentKeyData.max_budget !== null
|
||||
? `$${formatNumberWithCommas(currentKeyData.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>
|
||||
<Text>TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"}</Text>
|
||||
<Text>RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.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) => (
|
||||
{currentKeyData.models && currentKeyData.models.length > 0 ? (
|
||||
currentKeyData.models.map((model, index) => (
|
||||
<Badge key={index} color="red">
|
||||
{model}
|
||||
</Badge>
|
||||
@@ -333,17 +445,19 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
|
||||
<Card>
|
||||
<ObjectPermissionsView
|
||||
objectPermission={keyData.object_permission}
|
||||
objectPermission={currentKeyData.object_permission}
|
||||
variant="inline"
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<LoggingSettingsView
|
||||
loggingConfigs={extractLoggingSettings(keyData.metadata)}
|
||||
disabledCallbacks={Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
|
||||
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
|
||||
: []}
|
||||
loggingConfigs={extractLoggingSettings(currentKeyData.metadata)}
|
||||
disabledCallbacks={
|
||||
Array.isArray(currentKeyData.metadata?.litellm_disabled_callbacks)
|
||||
? mapInternalToDisplayNames(currentKeyData.metadata.litellm_disabled_callbacks)
|
||||
: []
|
||||
}
|
||||
variant="card"
|
||||
/>
|
||||
</Grid>
|
||||
@@ -363,7 +477,7 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
|
||||
{isEditing ? (
|
||||
<KeyEditView
|
||||
keyData={keyData}
|
||||
keyData={currentKeyData}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
onSubmit={handleKeyUpdate}
|
||||
teams={teams}
|
||||
@@ -375,49 +489,61 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">Key ID</Text>
|
||||
<Text className="font-mono">{keyData.token}</Text>
|
||||
<Text className="font-mono">{currentKeyData.token_id || currentKeyData.token}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Key Alias</Text>
|
||||
<Text>{keyData.key_alias || "Not Set"}</Text>
|
||||
<Text>{currentKeyData.key_alias || "Not Set"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Secret Key</Text>
|
||||
<Text className="font-mono">{keyData.key_name}</Text>
|
||||
<Text className="font-mono">{currentKeyData.key_name}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Team ID</Text>
|
||||
<Text>{keyData.team_id || "Not Set"}</Text>
|
||||
<Text>{currentKeyData.team_id || "Not Set"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Organization</Text>
|
||||
<Text>{keyData.organization_id || "Not Set"}</Text>
|
||||
<Text>{currentKeyData.organization_id || "Not Set"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Created</Text>
|
||||
<Text>{new Date(keyData.created_at).toLocaleString()}</Text>
|
||||
<Text>{formatTimestamp(currentKeyData.created_at)}</Text>
|
||||
</div>
|
||||
|
||||
{lastRegeneratedAt && (
|
||||
<div>
|
||||
<Text className="font-medium">Last Regenerated</Text>
|
||||
<div className="flex items-center gap-2">
|
||||
<Text>{formatTimestamp(lastRegeneratedAt)}</Text>
|
||||
<Badge color="green" size="xs">
|
||||
Recent
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Expires</Text>
|
||||
<Text>{keyData.expires ? new Date(keyData.expires).toLocaleString() : "Never"}</Text>
|
||||
<Text>{currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Spend</Text>
|
||||
<Text>${formatNumberWithCommas(keyData.spend, 4)} USD</Text>
|
||||
<Text>${formatNumberWithCommas(currentKeyData.spend, 4)} USD</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Budget</Text>
|
||||
<Text>
|
||||
{keyData.max_budget !== null
|
||||
? `$${formatNumberWithCommas(keyData.max_budget, 2)}`
|
||||
{currentKeyData.max_budget !== null
|
||||
? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}`
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
</div>
|
||||
@@ -425,12 +551,9 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
<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"
|
||||
>
|
||||
{currentKeyData.models && currentKeyData.models.length > 0 ? (
|
||||
currentKeyData.models.map((model, index) => (
|
||||
<span key={index} className="px-2 py-1 bg-blue-100 rounded text-xs">
|
||||
{model}
|
||||
</span>
|
||||
))
|
||||
@@ -442,34 +565,49 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||
|
||||
<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>
|
||||
<Text>TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"}</Text>
|
||||
<Text>RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"}</Text>
|
||||
<Text>
|
||||
Max Parallel Requests:{" "}
|
||||
{currentKeyData.max_parallel_requests !== null
|
||||
? currentKeyData.max_parallel_requests
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
<Text>
|
||||
Model TPM Limits:{" "}
|
||||
{currentKeyData.metadata?.model_tpm_limit
|
||||
? JSON.stringify(currentKeyData.metadata.model_tpm_limit)
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
<Text>
|
||||
Model RPM Limits:{" "}
|
||||
{currentKeyData.metadata?.model_rpm_limit
|
||||
? JSON.stringify(currentKeyData.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">
|
||||
{formatMetadataForDisplay(keyData.metadata)}
|
||||
{formatMetadataForDisplay(currentKeyData.metadata)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<ObjectPermissionsView
|
||||
objectPermission={keyData.object_permission}
|
||||
objectPermission={currentKeyData.object_permission}
|
||||
variant="inline"
|
||||
className="pt-4 border-t border-gray-200"
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
|
||||
<LoggingSettingsView
|
||||
loggingConfigs={extractLoggingSettings(
|
||||
keyData.metadata,
|
||||
)}
|
||||
disabledCallbacks={Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
|
||||
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
|
||||
: []}
|
||||
loggingConfigs={extractLoggingSettings(currentKeyData.metadata)}
|
||||
disabledCallbacks={
|
||||
Array.isArray(currentKeyData.metadata?.litellm_disabled_callbacks)
|
||||
? mapInternalToDisplayNames(currentKeyData.metadata.litellm_disabled_callbacks)
|
||||
: []
|
||||
}
|
||||
variant="inline"
|
||||
className="pt-4 border-t border-gray-200"
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface Team {
|
||||
|
||||
export interface KeyResponse {
|
||||
token: string;
|
||||
token_id: string;
|
||||
key_name: string;
|
||||
key_alias: string;
|
||||
spend: number;
|
||||
|
||||
@@ -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<KeyResponse>) => 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<string | null>(null);
|
||||
const [regenerateFormData, setRegenerateFormData] = useState<any>(null);
|
||||
const [newExpiryTime, setNewExpiryTime] = useState<string | null>(null);
|
||||
const [isRegenerating, setIsRegenerating] = useState(false);
|
||||
const [form] = Form.useForm()
|
||||
const [regeneratedKey, setRegeneratedKey] = useState<string | null>(null)
|
||||
const [regenerateFormData, setRegenerateFormData] = useState<any>(null)
|
||||
const [newExpiryTime, setNewExpiryTime] = useState<string | null>(null)
|
||||
const [isRegenerating, setIsRegenerating] = useState(false)
|
||||
|
||||
// Track whether this is the user's own authentication key
|
||||
const [isOwnKey, setIsOwnKey] = useState<boolean>(false)
|
||||
|
||||
// Keep track of the current valid access token locally
|
||||
const [currentAccessToken, setCurrentAccessToken] = useState<string | null>(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<KeyResponse> = {
|
||||
// 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 (
|
||||
<Modal
|
||||
|
||||
@@ -381,6 +381,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||
setCurrentOrg={setCurrentOrg}
|
||||
organizations={organizations}
|
||||
createClicked={createClicked}
|
||||
setAccessToken={setAccessToken}
|
||||
/>
|
||||
</Col>
|
||||
</Grid>
|
||||
|
||||
@@ -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<React.SetStateAction<any | null>>;
|
||||
data: any[] | null;
|
||||
data: KeyResponse[] | null
|
||||
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
|
||||
teams: Team[] | null;
|
||||
premiumUser: boolean;
|
||||
currentOrg: Organization | null;
|
||||
organizations: Organization[] | null;
|
||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>;
|
||||
selectedKeyAlias: string | null;
|
||||
setSelectedKeyAlias: Setter<string | null>;
|
||||
createClicked: boolean;
|
||||
selectedKeyAlias: string | null
|
||||
setSelectedKeyAlias: Setter<string | null>
|
||||
createClicked: boolean
|
||||
setAccessToken?: (token: string) => void
|
||||
}
|
||||
|
||||
interface ItemData {
|
||||
@@ -155,7 +156,8 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||
setCurrentOrg,
|
||||
selectedKeyAlias,
|
||||
setSelectedKeyAlias,
|
||||
createClicked
|
||||
createClicked,
|
||||
setAccessToken,
|
||||
}) => {
|
||||
const [isButtonClicked, setIsButtonClicked] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
@@ -178,10 +180,10 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||
|
||||
// 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<ViewKeyTableProps> = ({
|
||||
}
|
||||
|
||||
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<ViewKeyTableProps> = ({
|
||||
}
|
||||
|
||||
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<ViewKeyTableProps> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AllKeysTable
|
||||
<AllKeysTable
|
||||
keys={keys}
|
||||
setKeys={setKeys}
|
||||
isLoading={isLoading}
|
||||
@@ -394,6 +394,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||
selectedKeyAlias={selectedKeyAlias}
|
||||
setSelectedKeyAlias={setSelectedKeyAlias}
|
||||
premiumUser={premiumUser}
|
||||
setAccessToken={setAccessToken}
|
||||
/>
|
||||
|
||||
{isDeleteModalOpen && (
|
||||
|
||||
Reference in New Issue
Block a user