diff --git a/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/keys.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/keys.spec.ts index aba37e25be..14ceb1a4a6 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/keys.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/proxy-admin/keys.spec.ts @@ -61,10 +61,15 @@ test.describe("Proxy Admin - Keys", () => { await expect(page.getByText("Back to Keys")).toBeVisible({ timeout: 10_000 }); await page.getByRole("button", { name: "Regenerate Key" }).click(); - await page.getByRole("button", { name: "Regenerate", exact: true }).click(); - // Success shows "Copy Virtual Key" button in the regenerated key dialog - await expect(page.getByText("Copy Virtual Key")).toBeVisible({ timeout: 10_000 }); + // Scope to the modal — the Regenerate button has an icon whose aria-label + // ("sync") is concatenated into the button's accessible name, and the + // "Regenerate Key" button is still in the DOM behind the modal. + const modal = page.locator(".ant-modal:visible"); + await modal.getByRole("button", { name: /Regenerate/ }).click(); + + // Success view shows a Copy button in the footer (text varies between modal versions) + await expect(modal.getByRole("button", { name: /Copy.*Key/ })).toBeVisible({ timeout: 20_000 }); }); test("Update key TPM and RPM limits", async ({ page }) => { diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx new file mode 100644 index 0000000000..a69ae77924 --- /dev/null +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx @@ -0,0 +1,359 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils"; +import { RegenerateKeyModal } from "./RegenerateKeyModal"; +import { KeyResponse } from "../key_team_helpers/key_list"; + +// Mock the networking call +const mockRegenerateKeyCall = vi.fn(); +vi.mock("../networking", () => ({ + regenerateKeyCall: (...args: unknown[]) => mockRegenerateKeyCall(...args), +})); + +const makeToken = (overrides: Partial = {}): KeyResponse => + ({ + token: "token-hash-123", + token_id: "token-id-123", + key_name: "sk-test-key", + key_alias: "my-test-key", + max_budget: 100, + tpm_limit: 5000, + rpm_limit: 500, + duration: "30d", + expires: "2026-12-31T00:00:00Z", + ...overrides, + }) as KeyResponse; + +describe("RegenerateKeyModal", () => { + const mockOnClose = vi.fn(); + const mockOnKeyUpdate = vi.fn(); + + const defaultProps = { + selectedToken: makeToken(), + visible: true, + onClose: mockOnClose, + onKeyUpdate: mockOnKeyUpdate, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should render the modal with correct title", () => { + renderWithProviders(); + expect(screen.getByText("Regenerate Virtual Key")).toBeInTheDocument(); + }); + + it("should not render the modal when visible is false", () => { + renderWithProviders(); + expect(screen.queryByText("Regenerate Virtual Key")).not.toBeInTheDocument(); + }); + + it("should display the form with pre-filled values", () => { + renderWithProviders(); + + const keyAliasInput = screen.getByLabelText("Key Alias") as HTMLInputElement; + expect(keyAliasInput).toBeDisabled(); + expect(keyAliasInput).toHaveValue("my-test-key"); + }); + + it("should display the current expiry when token has expires", () => { + renderWithProviders(); + expect(screen.getByText(/Current expiry:/)).toBeInTheDocument(); + }); + + it("should display 'Never' when token has no expires", () => { + renderWithProviders(); + expect(screen.getByText("Current expiry: Never")).toBeInTheDocument(); + }); + + it("should show Cancel and Regenerate buttons in form view", () => { + renderWithProviders(); + expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /Regenerate/ })).toBeInTheDocument(); + }); + + it("should call onClose when Cancel is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + expect(mockOnClose).toHaveBeenCalledOnce(); + }); + + it("should call onClose when the X close button is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByRole("button", { name: "Close" })); + expect(mockOnClose).toHaveBeenCalledOnce(); + }); + + it("should render form fields for budget and rate limits", () => { + renderWithProviders(); + + expect(screen.getByText("Max Budget (USD)")).toBeInTheDocument(); + expect(screen.getByText("TPM Limit")).toBeInTheDocument(); + expect(screen.getByText("RPM Limit")).toBeInTheDocument(); + }); + + it("should render duration and grace period fields", () => { + renderWithProviders(); + + expect(screen.getByText("Expire Key")).toBeInTheDocument(); + expect(screen.getByText("Grace Period")).toBeInTheDocument(); + }); + + it("should display grace period recommendation text", () => { + renderWithProviders(); + expect(screen.getByText("Recommended: 24h to 72h for production keys")).toBeInTheDocument(); + }); + + it("should call regenerateKeyCall and show success view on successful regeneration", async () => { + const user = userEvent.setup(); + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-regenerated-key", + token: "new-token-hash", + }); + + renderWithProviders(); + + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + await waitFor(() => { + expect(mockRegenerateKeyCall).toHaveBeenCalledOnce(); + }); + + await waitFor(() => { + expect(screen.getByText("sk-new-regenerated-key")).toBeInTheDocument(); + }); + + expect(screen.getByText(/will not see it again/)).toBeInTheDocument(); + }); + + it("should show Close button after successful regeneration", async () => { + const user = userEvent.setup(); + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-regenerated-key", + token: "new-token-hash", + }); + + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + await waitFor(() => { + expect(screen.getByText("sk-new-regenerated-key")).toBeInTheDocument(); + }); + + // Should show Close buttons (footer + modal X), not Cancel/Regenerate + const closeButtons = screen.getAllByRole("button", { name: "Close" }); + expect(closeButtons.length).toBeGreaterThanOrEqual(1); + expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /Regenerate/ })).not.toBeInTheDocument(); + }); + + it("should show Copy Key button after successful regeneration", async () => { + const user = userEvent.setup(); + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-regenerated-key", + token: "new-token-hash", + }); + + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /Copy Key/ })).toBeInTheDocument(); + }); + }); + + it("should swap the Copy Key button to 'Copied' after clicking it", async () => { + const user = userEvent.setup(); + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-regenerated-key", + token: "new-token-hash", + }); + + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + const copyButton = await screen.findByRole("button", { name: /Copy Key/ }); + await user.click(copyButton); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /Copied/ })).toBeInTheDocument(); + }); + expect(screen.queryByRole("button", { name: /Copy Key/ })).not.toBeInTheDocument(); + }); + + it("should display the 'Virtual Key' label above the key in the success view", async () => { + const user = userEvent.setup(); + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-regenerated-key", + token: "new-token-hash", + }); + + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + await waitFor(() => { + expect(screen.getByText("Virtual Key")).toBeInTheDocument(); + }); + }); + + it("should call onKeyUpdate with updated data after successful regeneration", async () => { + const user = userEvent.setup(); + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-regenerated-key", + token: "new-token-hash", + }); + + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + await waitFor(() => { + expect(mockOnKeyUpdate).toHaveBeenCalledOnce(); + }); + + const updateCall = mockOnKeyUpdate.mock.calls[0][0]; + expect(updateCall.key_name).toBe("sk-new-regenerated-key"); + }); + + it.each([ + ["30s", /New expiry:/], + ["15m", /New expiry:/], + ["2h", /New expiry:/], + ["7d", /New expiry:/], + ["2w", /New expiry:/], + ["1mo", /New expiry:/], + ])("should compute a new expiry preview for duration '%s'", async (durationInput, expected) => { + const user = userEvent.setup(); + renderWithProviders(); + + const durationField = screen.getByPlaceholderText("e.g. 30s, 30h, 30d"); + await user.clear(durationField); + await user.type(durationField, durationInput); + + await waitFor(() => { + expect(screen.getByText(expected)).toBeInTheDocument(); + }); + }); + + it("should fall back to the previous expiry when duration is unparseable", async () => { + // Regression: if calculateNewExpiryTime returns null (unrecognised suffix), + // the payload should fall back to the previous expires rather than null. + const user = userEvent.setup(); + const previousExpires = "2026-12-31T00:00:00Z"; + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-regenerated-key", + token: "new-token-hash", + }); + + renderWithProviders( + , + ); + + const durationField = screen.getByPlaceholderText("e.g. 30s, 30h, 30d"); + await user.clear(durationField); + await user.type(durationField, "bogus"); + + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + await waitFor(() => { + expect(mockOnKeyUpdate).toHaveBeenCalledOnce(); + }); + + const updateCall = mockOnKeyUpdate.mock.calls[0][0]; + expect(updateCall.expires).toBe(previousExpires); + }); + + it("should pass form values to onKeyUpdate even when the API echoes back different limits", async () => { + // Regression: when the regenerate endpoint returns GenerateKeyResponse, it echoes + // back the existing max_budget / tpm_limit / rpm_limit. The modal must prefer the + // values the user just submitted, not whatever the server echoes. + const user = userEvent.setup(); + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-regenerated-key", + token: "new-token-hash", + // stale values echoed from the server + max_budget: 9999, + tpm_limit: 9999, + rpm_limit: 9999, + }); + + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + await waitFor(() => { + expect(mockOnKeyUpdate).toHaveBeenCalledOnce(); + }); + + const updateCall = mockOnKeyUpdate.mock.calls[0][0]; + // The form's pre-filled values (from makeToken) must win over the API echo. + expect(updateCall.max_budget).toBe(100); + expect(updateCall.tpm_limit).toBe(5000); + expect(updateCall.rpm_limit).toBe(500); + }); + + it("should display key alias in success view", async () => { + const user = userEvent.setup(); + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-regenerated-key", + token: "new-token-hash", + }); + + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + await waitFor(() => { + expect(screen.getByText("my-test-key")).toBeInTheDocument(); + }); + }); + + it("should display 'No alias set' when key has no alias", async () => { + const user = userEvent.setup(); + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-regenerated-key", + token: "new-token-hash", + }); + + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + await waitFor(() => { + expect(screen.getByText("No alias set")).toBeInTheDocument(); + }); + }); + + it("should not call regenerateKeyCall when selectedToken is null", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + // The form shouldn't even be populated, but we check the button doesn't trigger a call + const regenerateBtn = screen.queryByRole("button", { name: /Regenerate/ }); + if (regenerateBtn) { + await user.click(regenerateBtn); + } + + expect(mockRegenerateKeyCall).not.toHaveBeenCalled(); + }); + + it("should pass the correct token identifier to regenerateKeyCall", async () => { + const user = userEvent.setup(); + mockRegenerateKeyCall.mockResolvedValue({ + key: "sk-new-key", + token: "new-hash", + }); + + renderWithProviders(); + await user.click(screen.getByRole("button", { name: /Regenerate/ })); + + await waitFor(() => { + expect(mockRegenerateKeyCall).toHaveBeenCalledWith( + "123", // accessToken from mocked useAuthorized + "token-hash-123", // selectedToken.token + expect.any(Object), + ); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx new file mode 100644 index 0000000000..27f96eccab --- /dev/null +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx @@ -0,0 +1,279 @@ +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { CheckOutlined, CopyOutlined, SyncOutlined } from "@ant-design/icons"; +import { Alert, Button, Col, Flex, Form, Input, InputNumber, Modal, Row, Space, Typography } from "antd"; +import { add } from "date-fns"; +import { useEffect, useState } from "react"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { KeyResponse } from "../key_team_helpers/key_list"; +import NotificationManager from "../molecules/notifications_manager"; +import { regenerateKeyCall } from "../networking"; + +const { Text } = Typography; + +interface RegenerateKeyModalProps { + selectedToken: KeyResponse | null; + visible: boolean; + onClose: () => void; + onKeyUpdate?: (updatedKeyData: Partial) => void; +} + +export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdate }: RegenerateKeyModalProps) { + const { accessToken } = useAuthorized(); + 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 [copied, setCopied] = useState(false); + + useEffect(() => { + 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 || "", + grace_period: "", + }); + } + }, [visible, selectedToken, form, accessToken]); + + const calculateNewExpiryTime = (duration: string | undefined): string | null => { + if (!duration) return null; + + try { + const amount = parseInt(duration); + if (Number.isNaN(amount)) { + throw new Error("Invalid duration format"); + } + const now = new Date(); + // Check "mo" before "m" to avoid a false prefix match (e.g. "1mo" → minutes). + let newExpiry: Date; + if (duration.endsWith("mo")) { + newExpiry = add(now, { months: amount }); + } else if (duration.endsWith("s")) { + newExpiry = add(now, { seconds: amount }); + } else if (duration.endsWith("m")) { + newExpiry = add(now, { minutes: amount }); + } else if (duration.endsWith("h")) { + newExpiry = add(now, { hours: amount }); + } else if (duration.endsWith("d")) { + newExpiry = add(now, { days: amount }); + } else if (duration.endsWith("w")) { + newExpiry = add(now, { weeks: amount }); + } else { + throw new Error("Invalid duration format"); + } + + return newExpiry.toLocaleString(); + } catch (error) { + return null; + } + }; + + useEffect(() => { + if (regenerateFormData?.duration) { + setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration)); + } else { + setNewExpiryTime(null); + } + }, [regenerateFormData?.duration]); + + const handleRegenerateKey = async () => { + if (!selectedToken || !accessToken) return; + + setIsRegenerating(true); + try { + const formValues = await form.validateFields(); + + const response = await regenerateKeyCall( + accessToken, + selectedToken.token || selectedToken.token_id, + formValues, + ); + setRegeneratedKey(response.key); + NotificationManager.success("Virtual Key regenerated successfully"); + + // Build the update payload. Spread the API response first so any new + // fields it returns (new token, timestamps, etc.) are captured, then + // override with the explicit form values — the user's just-submitted + // edits must win over whatever the API echoes back. + const updatedKeyData: Partial = { + ...response, + token: response.token || response.key_id || selectedToken.token, + key_name: response.key, + max_budget: formValues.max_budget, + tpm_limit: formValues.tpm_limit, + rpm_limit: formValues.rpm_limit, + expires: formValues.duration + ? (calculateNewExpiryTime(formValues.duration) ?? selectedToken.expires) + : selectedToken.expires, + }; + + // Update the parent component with new key data + if (onKeyUpdate) { + onKeyUpdate(updatedKeyData); + } + + setIsRegenerating(false); + } catch (error) { + console.error("Error regenerating key:", error); + NotificationManager.fromBackend(error); + setIsRegenerating(false); // Reset regenerating state on error + } + }; + + const handleClose = () => { + setRegeneratedKey(null); + setIsRegenerating(false); + setCopied(false); + form.resetFields(); + onClose(); + }; + + const handleCopyKey = () => { + setCopied(true); + }; + + return ( + + + + + + , + ] + : [ + + + + , + ] + } + > + {regeneratedKey ? ( + + + + + + Key Alias + + {selectedToken?.key_alias || "No alias set"} + + + + + Virtual Key + +
+ {regeneratedKey} +
+
+
+ ) : ( +
{ + 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} + + )} + + } + > + + + + + + Recommended: 24h to 72h for production keys + + } + rules={[ + { + pattern: /^(\d+(s|m|h|d|w|mo))?$/, + message: "Must be a duration like 30s, 30m, 24h, 2d, 1w, or 1mo", + }, + ]} + > + + + + +
+ )} +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/organisms/regenerate_key_modal.tsx b/ui/litellm-dashboard/src/components/organisms/regenerate_key_modal.tsx deleted file mode 100644 index 2fad101c20..0000000000 --- a/ui/litellm-dashboard/src/components/organisms/regenerate_key_modal.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { Button, Col, Grid, Text, TextInput, Title } from "@tremor/react"; -import { Form, InputNumber, Modal } from "antd"; -import { add } from "date-fns"; -import { useEffect, useState } from "react"; -import { CopyToClipboard } from "react-copy-to-clipboard"; -import { KeyResponse } from "../key_team_helpers/key_list"; -import NotificationManager from "../molecules/notifications_manager"; -import { regenerateKeyCall } from "../networking"; - -interface RegenerateKeyModalProps { - selectedToken: KeyResponse | null; - visible: boolean; - onClose: () => void; - onKeyUpdate?: (updatedKeyData: Partial) => void; -} - -export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdate }: RegenerateKeyModalProps) { - const { accessToken } = useAuthorized(); - 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 && 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 || "", - grace_period: "", - }); - - // 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, accessToken]); - - useEffect(() => { - if (!visible) { - // Reset states when modal is closed - setRegeneratedKey(null); - setIsRegenerating(false); - setIsOwnKey(false); - setCurrentAccessToken(null); - form.resetFields(); - } - }, [visible, form]); - - const calculateNewExpiryTime = (duration: string | undefined): string | null => { - 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; - } - }; - - useEffect(() => { - if (regenerateFormData?.duration) { - setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration)); - } else { - setNewExpiryTime(null); - } - }, [regenerateFormData?.duration]); - - const handleRegenerateKey = async () => { - if (!selectedToken || !currentAccessToken) return; - - setIsRegenerating(true); - try { - 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); - NotificationManager.success("Virtual 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 - - // Update the parent component with new key data - if (onKeyUpdate) { - onKeyUpdate(updatedKeyData); - } - - setIsRegenerating(false); - } catch (error) { - console.error("Error regenerating key:", error); - NotificationManager.fromBackend(error); - setIsRegenerating(false); // Reset regenerating state on error - } - }; - - const handleClose = () => { - setRegeneratedKey(null); - setIsRegenerating(false); - setIsOwnKey(false); - setCurrentAccessToken(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 Virtual Key: -
-
{regeneratedKey}
-
- NotificationManager.success("Virtual 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}
} - - - -
- Recommended: 24h to 72h for production keys to allow seamless client migration. -
-
- )} -
- ); -} diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx index 590864637a..abbc92f021 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx @@ -196,7 +196,7 @@ vi.mock("lucide-react", async () => { }); // Heavy children -> async factories & local React -vi.mock("../organisms/regenerate_key_modal", async () => { +vi.mock("../organisms/RegenerateKeyModal", async () => { const React = await import("react"); function RegenerateKeyModal() { return null; diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index 24e3e18b93..5b5e7722c0 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -20,7 +20,7 @@ import NotificationManager from "../molecules/notifications_manager"; import { getPolicyInfoWithGuardrails, keyDeleteCall, keyUpdateCall } from "../networking"; import { useResetKeySpend } from "@/app/(dashboard)/hooks/keys/useResetKeySpend"; import ObjectPermissionsView from "../object_permissions_view"; -import { RegenerateKeyModal } from "../organisms/regenerate_key_modal"; +import { RegenerateKeyModal } from "../organisms/RegenerateKeyModal"; import { parseErrorMessage } from "../shared/errorUtils"; import { KeyEditView } from "./key_edit_view";