diff --git a/ui/litellm-dashboard/src/components/budgets/budget_panel.test.tsx b/ui/litellm-dashboard/src/components/budgets/budget_panel.test.tsx index d6412025054..534693d3984 100644 --- a/ui/litellm-dashboard/src/components/budgets/budget_panel.test.tsx +++ b/ui/litellm-dashboard/src/components/budgets/budget_panel.test.tsx @@ -1,5 +1,6 @@ import * as networking from "../networking"; import { fireEvent, render, waitFor, screen } from "@testing-library/react"; +import { act } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import BudgetPanel from "./budget_panel"; @@ -24,11 +25,11 @@ describe("Budget Panel", () => { }, ]); - const { getByText } = render(); + render(); await waitFor(() => { - expect(getByText("Create a budget to assign to customers.")).toBeInTheDocument(); - expect(getByText("budget-1")).toBeInTheDocument(); + expect(screen.getByText("Create a budget to assign to customers.")).toBeInTheDocument(); + expect(screen.getByText("budget-1")).toBeInTheDocument(); }); }); @@ -43,23 +44,102 @@ describe("Budget Panel", () => { }, ]); - const { getByText, container } = render(); + render(); await waitFor(() => { - expect(getByText("budget-to-delete")).toBeInTheDocument(); + expect(screen.getByText("budget-to-delete")).toBeInTheDocument(); }); - // Find the first table row in tbody and click the second icon (trash/delete) - const bodyRows = container.querySelectorAll("tbody tr"); - expect(bodyRows.length).toBeGreaterThan(0); - const firstRow = bodyRows[0]; - const rowClickableIcons = firstRow.querySelectorAll(".cursor-pointer"); - expect(rowClickableIcons.length).toBeGreaterThan(1); + const deleteButton = screen.getByTestId("delete-budget-button"); - fireEvent.click(rowClickableIcons[1]); + act(() => { + fireEvent.click(deleteButton); + }); await waitFor(() => { expect(screen.getByText("Delete Budget?")).toBeInTheDocument(); }); }); + + it("should successfully delete a budget", async () => { + vi.mocked(networking.getBudgetList).mockResolvedValue([ + { + budget_id: "budget-to-delete", + max_budget: "200", + rpm_limit: 20, + tpm_limit: 2000, + updated_at: "2024-01-02T00:00:00Z", + }, + ]); + vi.mocked(networking.budgetDeleteCall).mockResolvedValue(undefined); + + render(); + + await waitFor(() => { + expect(screen.getByText("budget-to-delete")).toBeInTheDocument(); + }); + + // Open delete modal + const deleteButton = screen.getByTestId("delete-budget-button"); + act(() => { + fireEvent.click(deleteButton); + }); + + await waitFor(() => { + expect(screen.getByText("Delete Budget?")).toBeInTheDocument(); + }); + + // Confirm delete + const confirmButton = screen.getByRole("button", { name: /delete/i }); + act(() => { + fireEvent.click(confirmButton); + }); + + await waitFor(() => { + expect(networking.budgetDeleteCall).toHaveBeenCalledWith("token-123", "budget-to-delete"); + expect(networking.getBudgetList).toHaveBeenCalledTimes(2); // Initial load + refresh after delete + }); + }); + + it("should handle delete error", async () => { + vi.mocked(networking.getBudgetList).mockResolvedValue([ + { + budget_id: "budget-to-delete", + max_budget: "200", + rpm_limit: 20, + tpm_limit: 2000, + updated_at: "2024-01-02T00:00:00Z", + }, + ]); + vi.mocked(networking.budgetDeleteCall).mockRejectedValue(new Error("Delete failed")); + + render(); + + await waitFor(() => { + expect(screen.getByText("budget-to-delete")).toBeInTheDocument(); + }); + + // Open delete modal + const deleteButton = screen.getByTestId("delete-budget-button"); + act(() => { + fireEvent.click(deleteButton); + }); + + await waitFor(() => { + expect(screen.getByText("Delete Budget?")).toBeInTheDocument(); + }); + + // Confirm delete + const confirmButton = screen.getByRole("button", { name: /delete/i }); + act(() => { + fireEvent.click(confirmButton); + }); + + await waitFor(() => { + expect(networking.budgetDeleteCall).toHaveBeenCalledWith("token-123", "budget-to-delete"); + }); + + // Modal should still be open (error handling) + expect(screen.getByText("Delete Budget?")).toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/components/budgets/budget_panel.tsx b/ui/litellm-dashboard/src/components/budgets/budget_panel.tsx index 252287191b7..b52ef5ab947 100644 --- a/ui/litellm-dashboard/src/components/budgets/budget_panel.tsx +++ b/ui/litellm-dashboard/src/components/budgets/budget_panel.tsx @@ -27,6 +27,7 @@ import NotificationsManager from "../molecules/notifications_manager"; import { budgetDeleteCall, getBudgetList } from "../networking"; import BudgetModal from "./budget_modal"; import EditBudgetModal from "./edit_budget_modal"; +import { CREATE_END_USER_CURL_COMMAND, CHAT_COMPLETIONS_CURL_COMMAND, OPENAI_SDK_PYTHON_CODE } from "./constants"; interface BudgetSettingsPageProps { accessToken: string | null; @@ -110,139 +111,111 @@ const BudgetPanel: React.FC = ({ accessToken }) => { - - {selectedBudget && ( - - )} - - Create a budget to assign to customers. - - - - Budget ID - Max Budget - TPM - RPM - - + + + Budgets + Examples + + + +
+ + {selectedBudget && ( + + )} + + Create a budget to assign to customers. +
+ + + Budget ID + Max Budget + TPM + RPM + + - - {budgetList - .slice() // Creates a shallow copy to avoid mutating the original array - .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) // Sort by updated_at in descending order - .map((value: budgetItem, index: number) => ( - - {value.budget_id} - {value.max_budget ? value.max_budget : "n/a"} - {value.tpm_limit ? value.tpm_limit : "n/a"} - {value.rpm_limit ? value.rpm_limit : "n/a"} - handleEditCall(value)} - /> - handleDeleteClick(value)} - /> - - ))} - -
-
- -
- How to use budget id - - - Assign Budget to Customer - Test it (Curl) - - Test it (OpenAI SDK) - - - - - {` -curl -X POST --location '/end_user/new' \ - --H 'Authorization: Bearer ' \ - --H 'Content-Type: application/json' \ - --d '{"user_id": "my-customer-id', "budget_id": ""}' # 👈 KEY CHANGE - - `} - - - - - {` -curl -X POST --location '/chat/completions' \ - --H 'Authorization: Bearer ' \ - --H 'Content-Type: application/json' \ - --d '{ - "model": "gpt-3.5-turbo', - "messages":[{"role": "user", "content": "Hey, how's it going?"}], - "user": "my-customer-id" -}' # 👈 KEY CHANGE - - `} - - - - - {`from openai import OpenAI -client = OpenAI( - base_url="", - api_key="" -) - -completion = client.chat.completions.create( - model="gpt-3.5-turbo", - messages=[ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Hello!"} - ], - user="my-customer-id" -) - -print(completion.choices[0].message)`} - - - - -
+ + {budgetList + .slice() // Creates a shallow copy to avoid mutating the original array + .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) // Sort by updated_at in descending order + .map((value: budgetItem, index: number) => ( + + {value.budget_id} + {value.max_budget ? value.max_budget : "n/a"} + {value.tpm_limit ? value.tpm_limit : "n/a"} + {value.rpm_limit ? value.rpm_limit : "n/a"} + handleEditCall(value)} + dataTestId="edit-budget-button" + /> + handleDeleteClick(value)} + dataTestId="delete-budget-button" + /> + + ))} + + + + + + + +
+ How to use budget id + + + Assign Budget to Customer + Test it (Curl) + Test it (OpenAI SDK) + + + + {CREATE_END_USER_CURL_COMMAND} + + + {CHAT_COMPLETIONS_CURL_COMMAND} + + + {OPENAI_SDK_PYTHON_CODE} + + + +
+
+ + ); }; diff --git a/ui/litellm-dashboard/src/components/budgets/constants.ts b/ui/litellm-dashboard/src/components/budgets/constants.ts new file mode 100644 index 00000000000..9d6736db1f1 --- /dev/null +++ b/ui/litellm-dashboard/src/components/budgets/constants.ts @@ -0,0 +1,42 @@ +export const CREATE_END_USER_CURL_COMMAND = ` +curl -X POST --location '/end_user/new' \\ + +-H 'Authorization: Bearer ' \\ + +-H 'Content-Type: application/json' \\ + +-d '{"user_id": "my-customer-id', "budget_id": ""}' # 👈 KEY CHANGE + +`; + +export const CHAT_COMPLETIONS_CURL_COMMAND = ` +curl -X POST --location '/chat/completions' \\ + +-H 'Authorization: Bearer ' \\ + +-H 'Content-Type: application/json' \\ + +-d '{ + "model": "gpt-3.5-turbo', + "messages":[{"role": "user", "content": "Hey, how's it going?"}], + "user": "my-customer-id" +}' # 👈 KEY CHANGE + +`; + +export const OPENAI_SDK_PYTHON_CODE = `from openai import OpenAI +client = OpenAI( + base_url="", + api_key="" +) + +completion = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"} + ], + user="my-customer-id" +) + +print(completion.choices[0].message)`;