From e42baeb5abf2ae37d64117dc70a9dc0138daf4d3 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Wed, 8 Apr 2026 23:52:25 -0700 Subject: [PATCH 01/13] [Refactor] UI - Virtual Keys: migrate regenerate key modal to AntD Replace Tremor components in the regenerate key modal with Ant Design equivalents and move the component to a new PascalCase file. The form layout now uses Row/Col to place Max Budget, TPM Limit, and RPM Limit on one row and Expire Key with Grace Period on another, reducing the vertical footprint. The success view shows an Alert banner, the key alias as secondary context, and the regenerated key in a monospace block with an inline primary Copy button. Also adds unit tests for the new component and updates the existing Playwright spec to match the new banner and button text. --- .../e2e_tests/tests/proxy-admin/keys.spec.ts | 5 +- .../organisms/RegenerateKeyModal.test.tsx | 271 ++++++++++++++++++ ...e_key_modal.tsx => RegenerateKeyModal.tsx} | 180 ++++++++---- .../KeyInfoView.handleKeyUpdate.test.tsx | 2 +- .../components/templates/key_info_view.tsx | 2 +- 5 files changed, 394 insertions(+), 66 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx rename ui/litellm-dashboard/src/components/organisms/{regenerate_key_modal.tsx => RegenerateKeyModal.tsx} (60%) 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..24e8d4f4b3 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 @@ -63,8 +63,9 @@ test.describe("Proxy Admin - Keys", () => { 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 }); + // Success view shows the warning banner and a Copy button for the regenerated key + await expect(page.getByText("Save it now, you will not see it again")).toBeVisible({ timeout: 10_000 }); + await expect(page.getByRole("button", { name: /Copy/ })).toBeVisible({ timeout: 10_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..1cb77bb9af --- /dev/null +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx @@ -0,0 +1,271 @@ +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), +})); + +// Mock CopyToClipboard to render a simple button +vi.mock("react-copy-to-clipboard", () => ({ + CopyToClipboard: ({ children, onCopy }: { children: React.ReactElement; onCopy: () => void }) => { + const React = require("react"); + return React.cloneElement(children, { onClick: onCopy }); + }, +})); + +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 Virtual 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/ })).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("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/regenerate_key_modal.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx similarity index 60% rename from ui/litellm-dashboard/src/components/organisms/regenerate_key_modal.tsx rename to ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx index 2fad101c20..e888713fe0 100644 --- a/ui/litellm-dashboard/src/components/organisms/regenerate_key_modal.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx @@ -1,6 +1,6 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { Button, Col, Grid, Text, TextInput, Title } from "@tremor/react"; -import { Form, InputNumber, Modal } from "antd"; +import { CopyOutlined, SyncOutlined } from "@ant-design/icons"; +import { Alert, Button, Col, 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"; @@ -8,6 +8,10 @@ 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; @@ -151,6 +155,7 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat title="Regenerate Virtual Key" open={visible} onCancel={handleClose} + width={520} footer={ regeneratedKey ? [ @@ -159,46 +164,69 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat , ] : [ - , - , + + + + , ] } > {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}
+
+ + +
+
Key Alias
+
+ {selectedToken?.key_alias || "No alias set"}
+
+ +
+ + {regeneratedKey} + NotificationManager.success("Virtual Key copied to clipboard")} > - + - - +
+
) : (
{ if ("duration" in changedValues) { setRegenerateFormData((prev: { duration?: string }) => ({ ...prev, duration: changedValues.duration })); @@ -206,41 +234,69 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat }} > - + - - - - - - - - - - - - -
- 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. -
+ + + + + + + + + + + + + + + + + + + + + + + + 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/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"; From 15f7cc913414ac017f86e832a889aa8470ecef25 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Apr 2026 18:43:28 -0700 Subject: [PATCH 02/13] refactor(ui): replace success-view divs in regenerate key modal with antd Use Flex, Typography.Paragraph (with copyable), and Typography.Text instead of raw divs + code block + CopyToClipboard wrapper. Drops the direct react-copy-to-clipboard dependency in this component in favor of antd's native copyable support. Also fixes two test issues surfaced when running the e2e locally: - RegenerateKeyModal.test.tsx no longer mocks react-copy-to-clipboard (the component no longer imports it), removing the CJS require() inside an ESM mock factory flagged by Greptile. - keys.spec.ts scopes the Regenerate and Copy lookups to the modal. The Regenerate button has an icon whose aria-label ("sync") is concatenated into the button's accessible name, so an exact-match lookup on "Regenerate" failed; and the new Paragraph copyable renders a generic "Copy" button that collided with the other copyable fields on the key info view. --- .../e2e_tests/tests/proxy-admin/keys.spec.ts | 11 ++- .../organisms/RegenerateKeyModal.test.tsx | 30 +------ .../organisms/RegenerateKeyModal.tsx | 83 ++++++------------- 3 files changed, 39 insertions(+), 85 deletions(-) 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 24e8d4f4b3..9c19bb9b88 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,11 +61,16 @@ 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(); + + // 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 the warning banner and a Copy button for the regenerated key - await expect(page.getByText("Save it now, you will not see it again")).toBeVisible({ timeout: 10_000 }); - await expect(page.getByRole("button", { name: /Copy/ })).toBeVisible({ timeout: 10_000 }); + await expect(modal.getByText("Save it now, you will not see it again")).toBeVisible({ timeout: 10_000 }); + await expect(modal.getByRole("button", { name: "Copy", exact: true })).toBeVisible({ timeout: 10_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 index 1cb77bb9af..d6eb7dd55a 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx @@ -10,14 +10,6 @@ vi.mock("../networking", () => ({ regenerateKeyCall: (...args: unknown[]) => mockRegenerateKeyCall(...args), })); -// Mock CopyToClipboard to render a simple button -vi.mock("react-copy-to-clipboard", () => ({ - CopyToClipboard: ({ children, onCopy }: { children: React.ReactElement; onCopy: () => void }) => { - const React = require("react"); - return React.cloneElement(children, { onClick: onCopy }); - }, -})); - const makeToken = (overrides: Partial = {}): KeyResponse => ({ token: "token-hash-123", @@ -71,12 +63,7 @@ describe("RegenerateKeyModal", () => { }); it("should display 'Never' when token has no expires", () => { - renderWithProviders( - , - ); + renderWithProviders(); expect(screen.getByText("Current expiry: Never")).toBeInTheDocument(); }); @@ -119,9 +106,7 @@ describe("RegenerateKeyModal", () => { it("should display grace period recommendation text", () => { renderWithProviders(); - expect( - screen.getByText("Recommended: 24h to 72h for production keys"), - ).toBeInTheDocument(); + expect(screen.getByText("Recommended: 24h to 72h for production keys")).toBeInTheDocument(); }); it("should call regenerateKeyCall and show success view on successful regeneration", async () => { @@ -222,12 +207,7 @@ describe("RegenerateKeyModal", () => { token: "new-token-hash", }); - renderWithProviders( - , - ); + renderWithProviders(); await user.click(screen.getByRole("button", { name: /Regenerate/ })); await waitFor(() => { @@ -237,9 +217,7 @@ describe("RegenerateKeyModal", () => { it("should not call regenerateKeyCall when selectedToken is null", async () => { const user = userEvent.setup(); - renderWithProviders( - , - ); + 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/ }); diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx index e888713fe0..c942832e9f 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx @@ -1,16 +1,13 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { CopyOutlined, SyncOutlined } from "@ant-design/icons"; -import { Alert, Button, Col, Form, Input, InputNumber, Modal, Row, Space, Typography } from "antd"; +import { 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; - - +const { Text, Paragraph } = Typography; interface RegenerateKeyModalProps { selectedToken: KeyResponse | null; @@ -174,54 +171,27 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat } > {regeneratedKey ? ( -
- + + -
-
Key Alias
-
- {selectedToken?.key_alias || "No alias set"} -
-
+ + + Key Alias + + {selectedToken?.key_alias || "No alias set"} + -
NotificationManager.success("Virtual Key copied to clipboard"), }} + style={{ marginBottom: 0, wordBreak: "break-all" }} > - - {regeneratedKey} - - NotificationManager.success("Virtual Key copied to clipboard")} - > - - -
-
+ {regeneratedKey} + + ) : (
+ - Current expiry: {selectedToken?.expires ? new Date(selectedToken.expires).toLocaleString() : "Never"} + Current expiry:{" "} + {selectedToken?.expires ? new Date(selectedToken.expires).toLocaleString() : "Never"} {newExpiryTime && ( -
- New expiry: {newExpiryTime} -
+ + New expiry: {newExpiryTime} + )} - +
} > From 839d9bd5f33ae238567925387dd619b79f4b51f5 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Apr 2026 20:14:01 -0700 Subject: [PATCH 03/13] refactor(ui): polish regenerate key success view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Label the key block with a small "Virtual Key" caption so the gray box is clearly the key container. - Move the Copy Key action to the modal footer as a primary button with icon; inline copy icon next to the key is removed. - Swap the button to "Copied" with a check icon on success instead of firing a notification — less noisy and keeps feedback in place. - Disable clicking outside the modal to close (maskClosable=false) so users must explicitly dismiss via Close or X. - Enlarge the key text and let its container span the full modal width. - Tests updated accordingly, including a new test for the copied-state swap and the "Virtual Key" label. --- .../e2e_tests/tests/proxy-admin/keys.spec.ts | 4 +- .../organisms/RegenerateKeyModal.test.tsx | 38 ++++++++++++- .../organisms/RegenerateKeyModal.tsx | 53 +++++++++++++------ 3 files changed, 76 insertions(+), 19 deletions(-) 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 9c19bb9b88..3b9da5d468 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 @@ -68,9 +68,9 @@ test.describe("Proxy Admin - Keys", () => { const modal = page.locator(".ant-modal:visible"); await modal.getByRole("button", { name: /Regenerate/ }).click(); - // Success view shows the warning banner and a Copy button for the regenerated key + // Success view shows the warning banner and a Copy Key button in the footer await expect(modal.getByText("Save it now, you will not see it again")).toBeVisible({ timeout: 10_000 }); - await expect(modal.getByRole("button", { name: "Copy", exact: true })).toBeVisible({ timeout: 10_000 }); + await expect(modal.getByRole("button", { name: /Copy Key/ })).toBeVisible({ timeout: 10_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 index d6eb7dd55a..1237082d2c 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx @@ -152,7 +152,7 @@ describe("RegenerateKeyModal", () => { expect(screen.queryByRole("button", { name: /Regenerate/ })).not.toBeInTheDocument(); }); - it("should show Copy Virtual Key button after successful regeneration", async () => { + it("should show Copy Key button after successful regeneration", async () => { const user = userEvent.setup(); mockRegenerateKeyCall.mockResolvedValue({ key: "sk-new-regenerated-key", @@ -163,7 +163,41 @@ describe("RegenerateKeyModal", () => { await user.click(screen.getByRole("button", { name: /Regenerate/ })); await waitFor(() => { - expect(screen.getByRole("button", { name: /Copy/ })).toBeInTheDocument(); + 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(); }); }); diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx index c942832e9f..3f254319bd 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx @@ -1,13 +1,14 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { SyncOutlined } from "@ant-design/icons"; +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, Paragraph } = Typography; +const { Text } = Typography; interface RegenerateKeyModalProps { selectedToken: KeyResponse | null; @@ -23,6 +24,7 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat const [regenerateFormData, setRegenerateFormData] = useState(null); const [newExpiryTime, setNewExpiryTime] = useState(null); const [isRegenerating, setIsRegenerating] = useState(false); + const [copied, setCopied] = useState(false); // Track whether this is the user's own authentication key const [isOwnKey, setIsOwnKey] = useState(false); @@ -57,6 +59,7 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat setIsRegenerating(false); setIsOwnKey(false); setCurrentAccessToken(null); + setCopied(false); form.resetFields(); } }, [visible, form]); @@ -143,22 +146,33 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat setIsRegenerating(false); setIsOwnKey(false); setCurrentAccessToken(null); + setCopied(false); form.resetFields(); onClose(); }; + const handleCopyKey = () => { + setCopied(true); + }; + return ( - Close - , + + + + + + , ] : [ @@ -181,16 +195,25 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat {selectedToken?.key_alias || "No alias set"} - NotificationManager.success("Virtual Key copied to clipboard"), - }} - style={{ marginBottom: 0, wordBreak: "break-all" }} - > - {regeneratedKey} - + + + Virtual Key + +
+ {regeneratedKey} +
+
) : ( Date: Thu, 9 Apr 2026 20:25:10 -0700 Subject: [PATCH 04/13] fix(ui): prefer form values over API echo in regenerate update payload The regenerate endpoint returns a GenerateKeyResponse that inherits max_budget/tpm_limit/rpm_limit from KeyRequestBase, so the API echoes the existing values back. The previous updatedKeyData layout spread ...response *after* the explicit formValues assignments, which meant the user's just-submitted edits were silently overwritten by the API echo before being propagated to the parent via onKeyUpdate. Reorder so the response spread comes first and the formValues-derived fields override it, and add a regression test that mocks a response with stale limits to lock the behavior in. Also drop the two leftover debug console.log statements. --- .../organisms/RegenerateKeyModal.test.tsx | 28 +++++++++++++++++++ .../organisms/RegenerateKeyModal.tsx | 17 +++++------ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx index 1237082d2c..f77cf787a3 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx @@ -219,6 +219,34 @@ describe("RegenerateKeyModal", () => { expect(updateCall.key_name).toBe("sk-new-regenerated-key"); }); + 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({ diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx index 3f254319bd..c714a15eb9 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx @@ -111,23 +111,20 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat 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 + // 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 = { - // 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 + ...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, - // 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); From 1d50f774e253909fdda1847fa27871e3e6cd5b59 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Apr 2026 20:31:35 -0700 Subject: [PATCH 05/13] fix(ui): support all duration suffixes in regenerate expiry preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit calculateNewExpiryTime only handled s/h/d, but the grace-period validation and backend accept m, w, and mo as well. Entering any of those in the Expire Key field caused the function to return null, which then propagated as expires: null in the onKeyUpdate payload — the parent UI would then render the expiry as "Never" even though the backend had correctly applied the new expiry. Extend the suffix check to cover s/m/h/d/w/mo, matching "mo" before "m" so "1mo" isn't misread as minutes. Also nullish-coalesce the call site so an unparseable duration falls back to the previous expiry instead of null. Add parametric tests for each supported suffix plus a regression test for the null fallback. --- .../organisms/RegenerateKeyModal.test.tsx | 48 +++++++++++++++++++ .../organisms/RegenerateKeyModal.tsx | 24 +++++++--- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx index f77cf787a3..a69ae77924 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx @@ -219,6 +219,54 @@ describe("RegenerateKeyModal", () => { 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 diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx index c714a15eb9..babbf9989e 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx @@ -68,15 +68,25 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat 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("s")) { - newExpiry = add(now, { seconds: parseInt(duration) }); + 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: parseInt(duration) }); + newExpiry = add(now, { hours: amount }); } else if (duration.endsWith("d")) { - newExpiry = add(now, { days: parseInt(duration) }); + newExpiry = add(now, { days: amount }); + } else if (duration.endsWith("w")) { + newExpiry = add(now, { weeks: amount }); } else { throw new Error("Invalid duration format"); } @@ -122,7 +132,9 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat max_budget: formValues.max_budget, tpm_limit: formValues.tpm_limit, rpm_limit: formValues.rpm_limit, - expires: formValues.duration ? calculateNewExpiryTime(formValues.duration) : selectedToken.expires, + expires: formValues.duration + ? (calculateNewExpiryTime(formValues.duration) ?? selectedToken.expires) + : selectedToken.expires, }; // Update the parent component with new key data From d0168bcff10e9550fa9fda815db8723e3f603f96 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Apr 2026 20:57:03 -0700 Subject: [PATCH 06/13] ci: retrigger e2e From ee374c48848f16bc39ff6c7abc536a6553f6754a Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Apr 2026 21:09:29 -0700 Subject: [PATCH 07/13] ci: pass LITELLM_LICENSE to e2e_ui_testing proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key regeneration is an enterprise feature — without LITELLM_LICENSE the endpoint returns a 403 and the Playwright test for "Regenerate key" never sees the success view. Other CircleCI jobs already pass this secret; the e2e_ui_testing job was missing it. --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2b0a6924cc..810727b011 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3201,6 +3201,7 @@ jobs: name: Start LiteLLM proxy environment: LITELLM_MASTER_KEY: "sk-1234" + LITELLM_LICENSE: ${LITELLM_LICENSE} MOCK_LLM_URL: "http://127.0.0.1:8090/v1" DISABLE_SCHEMA_UPDATE: "true" SERVER_ROOT_PATH: "" From cc43d09d79833fc69fbaf4b59ddc2ac5486c2e82 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 9 Apr 2026 21:19:02 -0700 Subject: [PATCH 08/13] Potential fix for pull request finding 'CodeQL / Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../src/components/organisms/RegenerateKeyModal.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx index babbf9989e..bbe3edceb6 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx @@ -26,9 +26,6 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat const [isRegenerating, setIsRegenerating] = useState(false); const [copied, setCopied] = 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); @@ -45,10 +42,6 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat // 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]); @@ -57,7 +50,6 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat // Reset states when modal is closed setRegeneratedKey(null); setIsRegenerating(false); - setIsOwnKey(false); setCurrentAccessToken(null); setCopied(false); form.resetFields(); From 9071dbba123d66cef07ab579b963cba8f7006ac9 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Apr 2026 21:24:22 -0700 Subject: [PATCH 09/13] fix(ui): remove leftover setIsOwnKey call after state removal --- .../src/components/organisms/RegenerateKeyModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx index bbe3edceb6..04e51a7a6f 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx @@ -145,7 +145,6 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat const handleClose = () => { setRegeneratedKey(null); setIsRegenerating(false); - setIsOwnKey(false); setCurrentAccessToken(null); setCopied(false); form.resetFields(); From d4288b4ff48d8e134813bd7da5816251f8b3939e Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Apr 2026 21:45:34 -0700 Subject: [PATCH 10/13] ci: fix LITELLM_LICENSE interpolation in e2e_ui_testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove LITELLM_LICENSE from the run step's environment block — YAML environment maps may pass the literal string "${LITELLM_LICENSE}" instead of interpolating the project env var, overriding it with a value that fails license validation. The project-level env var is inherited automatically by the proxy process. --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 810727b011..2b0a6924cc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3201,7 +3201,6 @@ jobs: name: Start LiteLLM proxy environment: LITELLM_MASTER_KEY: "sk-1234" - LITELLM_LICENSE: ${LITELLM_LICENSE} MOCK_LLM_URL: "http://127.0.0.1:8090/v1" DISABLE_SCHEMA_UPDATE: "true" SERVER_ROOT_PATH: "" From 89320a955c3211efbdafaf04430db825923710cf Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Apr 2026 23:21:01 -0700 Subject: [PATCH 11/13] fix(e2e): remove flaky banner check and increase regenerate key timeout --- .../e2e_tests/tests/proxy-admin/keys.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 3b9da5d468..be6f5cab47 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 @@ -68,9 +68,8 @@ test.describe("Proxy Admin - Keys", () => { const modal = page.locator(".ant-modal:visible"); await modal.getByRole("button", { name: /Regenerate/ }).click(); - // Success view shows the warning banner and a Copy Key button in the footer - await expect(modal.getByText("Save it now, you will not see it again")).toBeVisible({ timeout: 10_000 }); - await expect(modal.getByRole("button", { name: /Copy Key/ })).toBeVisible({ timeout: 10_000 }); + // Success view shows a Copy Key button in the footer + await expect(modal.getByRole("button", { name: /Copy Key/ })).toBeVisible({ timeout: 20_000 }); }); test("Update key TPM and RPM limits", async ({ page }) => { From 2e0af3795afbaef75f541fd5c6123cf043b70fa7 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Apr 2026 23:36:35 -0700 Subject: [PATCH 12/13] fix(e2e): broaden Copy Key button regex to match both modal versions On case-sensitive Linux CI, the old regenerate_key_modal.tsx from main can coexist with the new RegenerateKeyModal.tsx after merge. The old modal renders "Copy Virtual Key" while the new one renders "Copy Key". Use /Copy.*Key/ to match both. --- ui/litellm-dashboard/e2e_tests/tests/proxy-admin/keys.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 be6f5cab47..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 @@ -68,8 +68,8 @@ test.describe("Proxy Admin - Keys", () => { const modal = page.locator(".ant-modal:visible"); await modal.getByRole("button", { name: /Regenerate/ }).click(); - // Success view shows a Copy Key button in the footer - await expect(modal.getByRole("button", { name: /Copy Key/ })).toBeVisible({ timeout: 20_000 }); + // 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 }) => { From 7c6bd98fdf5492bece58d4b7b3a2dc3fb23e4794 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Sat, 11 Apr 2026 18:01:02 -0700 Subject: [PATCH 13/13] fix: address PR review comments on RegenerateKeyModal - Remove redundant useEffect cleanup that duplicated handleClose logic - Remove unnecessary currentAccessToken state, use accessToken from hook directly --- .../organisms/RegenerateKeyModal.tsx | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx index 04e51a7a6f..27f96eccab 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx @@ -26,9 +26,6 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat const [isRegenerating, setIsRegenerating] = useState(false); const [copied, setCopied] = useState(false); - // Keep track of the current valid access token locally - const [currentAccessToken, setCurrentAccessToken] = useState(null); - useEffect(() => { if (visible && selectedToken && accessToken) { form.setFieldsValue({ @@ -39,23 +36,9 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat duration: selectedToken.duration || "", grace_period: "", }); - - // Initialize the current access token - setCurrentAccessToken(accessToken); } }, [visible, selectedToken, form, accessToken]); - useEffect(() => { - if (!visible) { - // Reset states when modal is closed - setRegeneratedKey(null); - setIsRegenerating(false); - setCurrentAccessToken(null); - setCopied(false); - form.resetFields(); - } - }, [visible, form]); - const calculateNewExpiryTime = (duration: string | undefined): string | null => { if (!duration) return null; @@ -98,15 +81,14 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat }, [regenerateFormData?.duration]); const handleRegenerateKey = async () => { - if (!selectedToken || !currentAccessToken) return; + if (!selectedToken || !accessToken) return; setIsRegenerating(true); try { const formValues = await form.validateFields(); - // Use the current access token for the API call const response = await regenerateKeyCall( - currentAccessToken, + accessToken, selectedToken.token || selectedToken.token_id, formValues, ); @@ -145,7 +127,6 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat const handleClose = () => { setRegeneratedKey(null); setIsRegenerating(false); - setCurrentAccessToken(null); setCopied(false); form.resetFields(); onClose();