mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(ui): support all duration suffixes in regenerate expiry preview
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.
This commit is contained in:
parent
f95ef935ef
commit
1d50f774e2
2 changed files with 66 additions and 6 deletions
|
|
@ -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(<RegenerateKeyModal {...defaultProps} />);
|
||||
|
||||
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(
|
||||
<RegenerateKeyModal {...defaultProps} selectedToken={makeToken({ expires: previousExpires })} />,
|
||||
);
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue