diff --git a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.test.tsx b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.test.tsx
index 45121013652..896d8a14717 100644
--- a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.test.tsx
+++ b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.test.tsx
@@ -1,357 +1,214 @@
+import React, { useState } from "react";
+// eslint-disable-next-line no-restricted-imports -- exercising KeyLifecycleSettings requires hosting it in a real antd Form (the component it's built on)
+import { Form } from "antd";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi, beforeEach } from "vitest";
-import { renderWithProviders, screen } from "../../../tests/test-utils";
+import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils";
import KeyLifecycleSettings from "./KeyLifecycleSettings";
-vi.mock("antd", () => {
- const Option = ({ children, value }: any) => ;
- const Select = ({ children, value, onChange, placeholder }: any) => (
-
- );
- Select.Option = Option;
- return {
- Select,
- Tooltip: ({ children, title }: any) => (
-
- {children}
-
- ),
- Switch: ({ checked, onChange }: any) => (
- onChange(e.target.checked)} />
- ),
- Divider: () =>
,
- };
-});
+const CREATE_PLACEHOLDER = "e.g., 30d or leave empty to never expire";
+const EDIT_PLACEHOLDER = "e.g., 30d";
-vi.mock("@ant-design/icons", () => ({
- InfoCircleOutlined: () => ℹ,
-}));
+interface HarnessProps {
+ isCreateMode?: boolean;
+ onFinish?: (values: Record) => void;
+}
-vi.mock("@tremor/react", () => ({
- TextInput: ({ value, onValueChange, onChange, placeholder, name, className }: any) => {
- const handleChange = (e: React.ChangeEvent) => {
- if (onChange) {
- onChange(e);
- }
- if (onValueChange) {
- onValueChange(e.target.value);
- }
- };
- return (
- = ({ isCreateMode = true, onFinish = () => {} }) => {
+ const [form] = Form.useForm();
+ const [autoRotationEnabled, setAutoRotationEnabled] = useState(false);
+ const [rotationInterval, setRotationInterval] = useState("");
+ const [neverExpire, setNeverExpire] = useState(false);
+
+ return (
+
+ );
+};
+
+const getDurationInput = (isCreateMode = true) =>
+ screen.getByPlaceholderText(isCreateMode ? CREATE_PLACEHOLDER : EDIT_PLACEHOLDER) as HTMLInputElement;
describe("KeyLifecycleSettings", () => {
- const mockForm = {
- getFieldValue: vi.fn(),
- setFieldValue: vi.fn(),
- setFieldsValue: vi.fn(),
- };
-
- const defaultProps = {
- form: mockForm,
- autoRotationEnabled: false,
- onAutoRotationChange: vi.fn(),
- rotationInterval: "",
- onRotationIntervalChange: vi.fn(),
- isCreateMode: false,
- };
-
beforeEach(() => {
vi.clearAllMocks();
- mockForm.getFieldValue.mockReturnValue("");
});
- it("should render without crashing", () => {
- renderWithProviders();
-
+ it("renders the expiry and auto-rotation sections", () => {
+ renderWithProviders();
expect(screen.getByText("Key Expiry Settings")).toBeInTheDocument();
expect(screen.getByText("Auto-Rotation Settings")).toBeInTheDocument();
+ expect(getDurationInput()).toBeInTheDocument();
});
- describe("Key Expiry Settings", () => {
- it("should render expiry input field", () => {
- renderWithProviders();
+ it("uses the create-mode placeholder in create mode", () => {
+ renderWithProviders();
+ expect(screen.getByPlaceholderText(CREATE_PLACEHOLDER)).toBeInTheDocument();
+ });
- expect(screen.getByText("Expire Key")).toBeInTheDocument();
- expect(screen.getByTestId("duration-input")).toBeInTheDocument();
- });
+ it("uses the edit-mode placeholder in edit mode", () => {
+ renderWithProviders();
+ expect(screen.getByPlaceholderText(EDIT_PLACEHOLDER)).toBeInTheDocument();
+ });
- it("should show correct placeholder in create mode", () => {
- renderWithProviders();
-
- const input = screen.getByTestId("duration-input");
- expect(input).toHaveAttribute("placeholder", "e.g., 30d or leave empty to never expire");
- });
-
- it("should show correct placeholder in edit mode", () => {
- renderWithProviders();
-
- const input = screen.getByTestId("duration-input");
- expect(input).toHaveAttribute("placeholder", "e.g., 30d");
- });
-
- it("should show correct tooltip in create mode", () => {
- renderWithProviders();
-
- const tooltips = screen.getAllByTestId("tooltip");
- const expiryTooltip = tooltips.find((tooltip) =>
- tooltip.getAttribute("title")?.includes("Leave empty to keep the current expiry unchanged"),
- );
- expect(expiryTooltip).toBeInTheDocument();
- expect(expiryTooltip).toHaveAttribute(
- "title",
- "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to keep the current expiry unchanged.",
- );
- });
-
- it("should show correct tooltip in edit mode", () => {
- renderWithProviders();
-
- const tooltips = screen.getAllByTestId("tooltip");
- const expiryTooltip = tooltips.find((tooltip) =>
- tooltip.getAttribute("title")?.includes("Leave empty to keep the current expiry unchanged"),
- );
- expect(expiryTooltip).toBeInTheDocument();
- expect(expiryTooltip).toHaveAttribute(
- "title",
- "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to keep the current expiry unchanged.",
- );
- });
-
- it("should initialize with form value if present", () => {
- mockForm.getFieldValue.mockReturnValue("30d");
- renderWithProviders();
-
- const input = screen.getByTestId("duration-input") as HTMLInputElement;
- expect(input.value).toBe("30d");
- });
-
- it("should update form using setFieldValue when duration changes", async () => {
+ describe("duration is a single source of truth (regression for pre-filled value dropped on submit)", () => {
+ it("submits the duration the user typed", async () => {
const user = userEvent.setup();
- renderWithProviders();
+ const onFinish = vi.fn();
+ renderWithProviders();
- const input = screen.getByTestId("duration-input");
- await user.type(input, "60d");
+ await user.type(getDurationInput(), "1d");
+ await user.click(screen.getByRole("button", { name: "submit" }));
- expect(mockForm.setFieldValue).toHaveBeenCalledWith("duration", "60d");
+ await waitFor(() => expect(onFinish).toHaveBeenCalledTimes(1));
+ expect(onFinish.mock.calls[0][0]).toMatchObject({ duration: "1d" });
});
- it("should update form using setFieldsValue when setFieldValue is not available", async () => {
+ it("clears the displayed value when the form is reset, so no stale value lingers", async () => {
const user = userEvent.setup();
- const formWithoutSetFieldValue = {
- getFieldValue: vi.fn().mockReturnValue(""),
- setFieldsValue: vi.fn(),
- };
- renderWithProviders();
+ renderWithProviders();
- const input = screen.getByTestId("duration-input");
- await user.type(input, "90d");
+ await user.type(getDurationInput(), "1d");
+ expect(getDurationInput().value).toBe("1d");
- expect(formWithoutSetFieldValue.setFieldsValue).toHaveBeenCalledWith({ duration: "90d" });
+ await user.click(screen.getByRole("button", { name: "reset" }));
+
+ await waitFor(() => expect(getDurationInput().value).toBe(""));
+ });
+
+ it("never submits a value that differs from what is displayed after a reset", async () => {
+ const user = userEvent.setup();
+ const onFinish = vi.fn();
+ renderWithProviders();
+
+ // First create: type "1d" and submit -> "1d" is sent.
+ await user.type(getDurationInput(), "1d");
+ await user.click(screen.getByRole("button", { name: "submit" }));
+ await waitFor(() => expect(onFinish).toHaveBeenCalledTimes(1));
+ expect(onFinish.mock.calls[0][0]).toMatchObject({ duration: "1d" });
+
+ // Second create: form resets, so the field must show empty AND submit empty.
+ // The old bug showed a stale "1d" while submitting null/empty.
+ await user.click(screen.getByRole("button", { name: "reset" }));
+ await waitFor(() => expect(getDurationInput().value).toBe(""));
+
+ await user.click(screen.getByRole("button", { name: "submit" }));
+ await waitFor(() => expect(onFinish).toHaveBeenCalledTimes(2));
+ expect(onFinish.mock.calls[1][0].duration).not.toBe("1d");
+ expect(getDurationInput().value).toBe(onFinish.mock.calls[1][0].duration ?? "");
});
});
- describe("Auto-Rotation Settings", () => {
- it("should render auto-rotation switch", () => {
- renderWithProviders();
-
- expect(screen.getByText("Enable Auto-Rotation")).toBeInTheDocument();
- expect(screen.getByTestId("switch")).toBeInTheDocument();
- });
-
- it("should show switch as unchecked when autoRotationEnabled is false", () => {
- renderWithProviders();
-
- const switchElement = screen.getByTestId("switch") as HTMLInputElement;
- expect(switchElement.checked).toBe(false);
- });
-
- it("should show switch as checked when autoRotationEnabled is true", () => {
- renderWithProviders();
-
- const switchElement = screen.getByTestId("switch") as HTMLInputElement;
- expect(switchElement.checked).toBe(true);
- });
-
- it("should call onAutoRotationChange when switch is toggled", async () => {
+ describe("Never Expire", () => {
+ it("clears and disables the duration input, then submits an empty duration", async () => {
const user = userEvent.setup();
- const onAutoRotationChange = vi.fn();
- renderWithProviders();
+ const onFinish = vi.fn();
+ renderWithProviders();
- const switchElement = screen.getByTestId("switch");
- await user.click(switchElement);
+ await user.type(getDurationInput(false), "30d");
+ expect(getDurationInput(false).value).toBe("30d");
- expect(onAutoRotationChange).toHaveBeenCalledWith(true);
+ await user.click(screen.getByRole("checkbox", { name: /never expire/i }));
+
+ await waitFor(() => expect(getDurationInput(false).value).toBe(""));
+ expect(getDurationInput(false)).toBeDisabled();
+
+ await user.click(screen.getByRole("button", { name: "submit" }));
+ await waitFor(() => expect(onFinish).toHaveBeenCalledTimes(1));
+ expect(onFinish.mock.calls[0][0]).toMatchObject({ duration: "" });
});
+ });
- it("should not show rotation interval section when auto-rotation is disabled", () => {
- renderWithProviders();
+ describe("Auto-Rotation", () => {
+ it("reveals the rotation interval controls when enabled", async () => {
+ const user = userEvent.setup();
+ renderWithProviders();
expect(screen.queryByText("Rotation Interval")).not.toBeInTheDocument();
- expect(screen.queryByTestId("select")).not.toBeInTheDocument();
+ await user.click(screen.getByRole("switch"));
+
+ await waitFor(() => expect(screen.getByText("Rotation Interval")).toBeInTheDocument());
});
- it("should show rotation interval section when auto-rotation is enabled", () => {
- renderWithProviders();
-
- expect(screen.getByText("Rotation Interval")).toBeInTheDocument();
- expect(screen.getByTestId("select")).toBeInTheDocument();
- });
-
- it("should show all predefined interval options", () => {
- renderWithProviders();
-
- expect(screen.getByText("7 days")).toBeInTheDocument();
- expect(screen.getByText("30 days")).toBeInTheDocument();
- expect(screen.getByText("90 days")).toBeInTheDocument();
- expect(screen.getByText("180 days")).toBeInTheDocument();
- expect(screen.getByText("365 days")).toBeInTheDocument();
- expect(screen.getByText("Custom interval")).toBeInTheDocument();
- });
-
- it("should display current rotation interval in select", () => {
- renderWithProviders();
-
- const select = screen.getByTestId("select") as HTMLSelectElement;
- expect(select.value).toBe("90d");
- });
-
- it("should call onRotationIntervalChange when predefined interval is selected", async () => {
+ it("propagates a selected predefined interval", async () => {
const user = userEvent.setup();
- const onRotationIntervalChange = vi.fn();
- renderWithProviders(
- ,
- );
+ renderWithProviders();
- const select = screen.getByTestId("select");
- await user.selectOptions(select, "30d");
+ await user.click(screen.getByRole("switch"));
+ await waitFor(() => expect(screen.getByText("Rotation Interval")).toBeInTheDocument());
- expect(onRotationIntervalChange).toHaveBeenCalledWith("30d");
+ await user.click(screen.getByRole("combobox"));
+ await user.click(await screen.findByText("90 days"));
+
+ await waitFor(() => expect(document.querySelector(".ant-select-selection-item")?.textContent).toBe("90 days"));
+ expect(screen.getByTestId("rotation-interval-value")).toHaveTextContent("90d");
});
- it("should show custom input when custom option is selected", async () => {
+ it("shows the custom interval input when Custom interval is selected, without propagating yet", async () => {
const user = userEvent.setup();
- renderWithProviders();
+ renderWithProviders();
- const select = screen.getByTestId("select");
- await user.selectOptions(select, "custom");
+ await user.click(screen.getByRole("switch"));
+ await waitFor(() => expect(screen.getByText("Rotation Interval")).toBeInTheDocument());
- expect(screen.getByTestId("custom-interval-input")).toBeInTheDocument();
+ await user.click(screen.getByRole("combobox"));
+ await user.click(await screen.findByText("Custom interval"));
+
+ expect(await screen.findByPlaceholderText("e.g., 1s, 5m, 2h, 14d")).toBeInTheDocument();
expect(screen.getByText("Supported formats: seconds (s), minutes (m), hours (h), days (d)")).toBeInTheDocument();
+ expect(screen.getByTestId("rotation-interval-value")).toHaveTextContent("");
});
- it("should hide custom input when predefined interval is selected after custom", async () => {
+ it("propagates a typed custom interval to the parent", async () => {
const user = userEvent.setup();
- const onRotationIntervalChange = vi.fn();
- renderWithProviders(
- ,
- );
+ renderWithProviders();
- const select = screen.getByTestId("select");
- await user.selectOptions(select, "7d");
+ await user.click(screen.getByRole("switch"));
+ await waitFor(() => expect(screen.getByText("Rotation Interval")).toBeInTheDocument());
- expect(screen.queryByTestId("custom-interval-input")).not.toBeInTheDocument();
- expect(onRotationIntervalChange).toHaveBeenCalledWith("7d");
- });
+ await user.click(screen.getByRole("combobox"));
+ await user.click(await screen.findByText("Custom interval"));
- it("should call onRotationIntervalChange when custom interval is entered", async () => {
- const user = userEvent.setup();
- const onRotationIntervalChange = vi.fn();
- renderWithProviders(
- ,
- );
-
- const select = screen.getByTestId("select");
- await user.selectOptions(select, "custom");
-
- const customInput = screen.getByTestId("custom-interval-input");
+ const customInput = await screen.findByPlaceholderText("e.g., 1s, 5m, 2h, 14d");
await user.type(customInput, "14d");
- expect(onRotationIntervalChange).toHaveBeenCalledWith("14d");
+ await waitFor(() => expect(screen.getByTestId("rotation-interval-value")).toHaveTextContent("14d"));
+ expect((customInput as HTMLInputElement).value).toBe("14d");
});
- it("should show info message when auto-rotation is enabled", () => {
- renderWithProviders();
-
- expect(
- screen.getByText(
- "When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated after a brief grace period.",
- ),
- ).toBeInTheDocument();
- });
-
- it("should not show info message when auto-rotation is disabled", () => {
- renderWithProviders();
-
- expect(
- screen.queryByText(
- "When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated after a brief grace period.",
- ),
- ).not.toBeInTheDocument();
- });
-
- it("should initialize with custom interval input visible when custom interval is provided", () => {
- renderWithProviders();
-
- expect(screen.getByTestId("custom-interval-input")).toBeInTheDocument();
- const customInput = screen.getByTestId("custom-interval-input") as HTMLInputElement;
- expect(customInput.value).toBe("14d");
- });
-
- it("should show custom option selected when custom interval is provided", () => {
- renderWithProviders();
-
- const select = screen.getByTestId("select") as HTMLSelectElement;
- expect(select.value).toBe("custom");
- });
-
- it("should not call onRotationIntervalChange when selecting custom option", async () => {
+ it("hides the custom input and propagates the value when switching back to a predefined interval", async () => {
const user = userEvent.setup();
- const onRotationIntervalChange = vi.fn();
- renderWithProviders(
- ,
- );
+ renderWithProviders();
- const select = screen.getByTestId("select");
- await user.selectOptions(select, "custom");
+ await user.click(screen.getByRole("switch"));
+ await waitFor(() => expect(screen.getByText("Rotation Interval")).toBeInTheDocument());
- expect(onRotationIntervalChange).not.toHaveBeenCalled();
+ await user.click(screen.getByRole("combobox"));
+ await user.click(await screen.findByText("Custom interval"));
+ const customInput = await screen.findByPlaceholderText("e.g., 1s, 5m, 2h, 14d");
+ await user.type(customInput, "14d");
+ await waitFor(() => expect(screen.getByTestId("rotation-interval-value")).toHaveTextContent("14d"));
+
+ await user.click(screen.getByRole("combobox"));
+ await user.click(await screen.findByText("7 days"));
+
+ await waitFor(() => expect(screen.getByTestId("rotation-interval-value")).toHaveTextContent("7d"));
+ expect(screen.queryByPlaceholderText("e.g., 1s, 5m, 2h, 14d")).not.toBeInTheDocument();
});
});
});
diff --git a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx
index 7c4738f9ede..8e88fab1095 100644
--- a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx
+++ b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx
@@ -1,5 +1,5 @@
import React, { useState } from "react";
-import { Select, Tooltip, Divider, Switch, Checkbox } from "antd";
+import { Select, Tooltip, Divider, Switch, Checkbox, Form } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { TextInput } from "@tremor/react";
@@ -34,7 +34,6 @@ const KeyLifecycleSettings: React.FC = ({
const [showCustomInput, setShowCustomInput] = useState(isCustomInterval);
const [customInterval, setCustomInterval] = useState(isCustomInterval ? rotationInterval : "");
- const [durationValue, setDurationValue] = useState(form?.getFieldValue?.("duration") || "");
const handleIntervalChange = (value: string) => {
if (value === "custom") {
@@ -53,14 +52,6 @@ const KeyLifecycleSettings: React.FC = ({
onRotationIntervalChange(value);
};
- const handleDurationChange = (value: string) => {
- setDurationValue(value);
- if (form && typeof form.setFieldValue === "function") {
- form.setFieldValue("duration", value);
- } else if (form && typeof form.setFieldsValue === "function") {
- form.setFieldsValue({ duration: value });
- }
- };
return (
{/* Key Expiry Section */}
@@ -80,7 +71,6 @@ const KeyLifecycleSettings: React.FC = ({
const checked = e.target.checked;
onNeverExpireChange(checked);
if (checked) {
- setDurationValue("");
if (form && typeof form.setFieldValue === "function") {
form.setFieldValue("duration", "");
} else if (form && typeof form.setFieldsValue === "function") {
@@ -94,14 +84,13 @@ const KeyLifecycleSettings: React.FC = ({
)}
-
+
+
+
diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
index 2a04bad6aa8..711652eb783 100644
--- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
+++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
@@ -1644,9 +1644,6 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp
/>
-
-
-
diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
index a9fa05d817d..962c6bc3568 100644
--- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
@@ -860,9 +860,6 @@ export function KeyEditView({
neverExpire={neverExpire}
onNeverExpireChange={setNeverExpire}
/>
-
-
-
{/* Hidden form field for token */}