diff --git a/ui/litellm-dashboard/docs/CYCLES.md b/ui/litellm-dashboard/docs/CYCLES.md index 4e4f3c71a28..acb0f9a426d 100644 --- a/ui/litellm-dashboard/docs/CYCLES.md +++ b/ui/litellm-dashboard/docs/CYCLES.md @@ -36,3 +36,10 @@ Layer abbreviations: **TS** (tsc --noEmit), **Lint** (eslint), **Vitest** (Table + @tanstack/react-table, Popover, Tooltip, Badge variants, Skeleton) covered every antd / @tremor / @heroicons import encountered. +## 3. Budgets (experimental/budgets) + +- Cycles used: 1 / 7 +- Layer outcomes per cycle: + - cycle 1: TS ✓ | Lint ✓ (pre-existing unused-imports in test excluded) | Vitest ✓ (6/6) | Parity ⏭ | Snap ⏭ +- Final status: **done** + diff --git a/ui/litellm-dashboard/src/components/budgets/budget_modal.tsx b/ui/litellm-dashboard/src/components/budgets/budget_modal.tsx index b5ad8aaff34..46fcf89aa45 100644 --- a/ui/litellm-dashboard/src/components/budgets/budget_modal.tsx +++ b/ui/litellm-dashboard/src/components/budgets/budget_modal.tsx @@ -1,95 +1,212 @@ import React from "react"; -import { TextInput, Accordion, AccordionHeader, AccordionBody } from "@tremor/react"; -import { Button as Button2, Modal, Form, InputNumber, Select } from "antd"; import { useCreateBudget } from "@/app/(dashboard)/hooks/budgets/useBudgets"; import NotificationsManager from "../molecules/notifications_manager"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Controller, FormProvider, useForm } from "react-hook-form"; interface BudgetModalProps { isModalVisible: boolean; setIsModalVisible: React.Dispatch>; } -const BudgetModal: React.FC = ({ isModalVisible, setIsModalVisible }) => { - const [form] = Form.useForm(); - const createBudget = useCreateBudget(); - const handleOk = () => { - setIsModalVisible(false); - form.resetFields(); - }; +type BudgetFormValues = { + budget_id: string; + tpm_limit: number | null; + rpm_limit: number | null; + max_budget: number | null; + budget_duration: string | null; +}; + +const defaultValues: BudgetFormValues = { + budget_id: "", + tpm_limit: null, + rpm_limit: null, + max_budget: null, + budget_duration: null, +}; + +function LabeledRow({ + id, + label, + help, + children, + required, + error, +}: { + id?: string; + label: string; + help?: string; + children: React.ReactNode; + required?: boolean; + error?: string; +}) { + return ( +
+ +
+ {children} + {help &&

{help}

} + {error &&

{error}

} +
+
+ ); +} + +const BudgetModal: React.FC = ({ + isModalVisible, + setIsModalVisible, +}) => { + const form = useForm({ defaultValues, mode: "onSubmit" }); + const createBudget = useCreateBudget(); const handleCancel = () => { setIsModalVisible(false); - form.resetFields(); + form.reset(defaultValues); }; - const handleCreate = async (formValues: Record) => { + const handleCreate = form.handleSubmit(async (values) => { try { NotificationsManager.info("Making API Call"); - await createBudget.mutateAsync(formValues); + await createBudget.mutateAsync(values); NotificationsManager.success("Budget Created"); - form.resetFields(); + form.reset(defaultValues); setIsModalVisible(false); } catch (error) { console.error("Error creating the budget:", error); NotificationsManager.fromBackend(`Error creating the budget: ${error}`); } - }; + }); return ( - (!o ? handleCancel() : undefined)} > -
- <> - - - - - - - - - + + + Create Budget + + Create a new spend / rate-limit budget. + + + + + + + + + + + + + - - - Optional Settings - - - - - - - - - - - + + + + Optional Settings + + + + + + + ( + + )} + /> + + + + -
- Create Budget -
- -
+ + + + + + + + ); }; diff --git a/ui/litellm-dashboard/src/components/budgets/budget_panel.tsx b/ui/litellm-dashboard/src/components/budgets/budget_panel.tsx index e42d0569652..e02857a6d1c 100644 --- a/ui/litellm-dashboard/src/components/budgets/budget_panel.tsx +++ b/ui/litellm-dashboard/src/components/budgets/budget_panel.tsx @@ -1,24 +1,19 @@ /** - * The parent pane, showing list of budgets - * + * The parent pane, showing list of budgets. + * Migrated to shadcn in phase 1. See docs/BLUEPRINT.md. */ +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { - Button, - Card, - Tab, - TabGroup, Table, TableBody, TableCell, TableHead, - TableHeaderCell, + TableHeader, TableRow, - TabList, - TabPanel, - TabPanels, - Text, -} from "@tremor/react"; +} from "@/components/ui/table"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import React, { useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import DeleteResourceModal from "../common_components/DeleteResourceModal"; @@ -27,7 +22,11 @@ import NotificationsManager from "../molecules/notifications_manager"; import { useBudgets, useDeleteBudget } from "@/app/(dashboard)/hooks/budgets/useBudgets"; 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"; +import { + CREATE_END_USER_CURL_COMMAND, + CHAT_COMPLETIONS_CURL_COMMAND, + OPENAI_SDK_PYTHON_CODE, +} from "./constants"; interface BudgetSettingsPageProps { accessToken: string | null; @@ -50,10 +49,8 @@ const BudgetPanel: React.FC = ({ accessToken }) => { const { data: budgetList = [] } = useBudgets(); const deleteBudget = useDeleteBudget(); - const handleEditCall = async (budget: budgetItem) => { - if (accessToken == null) { - return; - } + const handleEditCall = (budget: budgetItem) => { + if (accessToken == null) return; setSelectedBudget(budget); setIsEditModalVisible(true); }; @@ -64,9 +61,7 @@ const BudgetPanel: React.FC = ({ accessToken }) => { }; const handleDeleteConfirm = async () => { - if (!selectedBudget || accessToken == null) { - return; - } + if (!selectedBudget || accessToken == null) return; try { await deleteBudget.mutateAsync(selectedBudget.budget_id); NotificationsManager.success("Budget deleted."); @@ -83,115 +78,132 @@ const BudgetPanel: React.FC = ({ accessToken }) => { } }; - const handleDeleteCancel = () => { - setIsDeleteModalVisible(false); - }; + const handleDeleteCancel = () => setIsDeleteModalVisible(false); return (
- - - - Budgets - Examples - - - -
- + + Budgets + Examples + + +
+ + {selectedBudget && ( + - {selectedBudget && ( - - )} - - Create a budget to assign to customers. - - - - Budget ID - Max Budget - TPM - RPM - - + )} + +

+ Create a budget to assign to customers. +

+
+ + + Budget ID + Max Budget + TPM + RPM + Actions + + - - {budgetList - .slice() - .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) - .map((value: budgetItem) => ( - - {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} - - - -
-
- - + + {budgetList + .slice() + .sort( + (a, b) => + new Date(b.updated_at).getTime() - + new Date(a.updated_at).getTime(), + ) + .map((value: budgetItem) => ( + + {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/edit_budget_modal.tsx b/ui/litellm-dashboard/src/components/budgets/edit_budget_modal.tsx index 5fb763bb959..24d338a4977 100644 --- a/ui/litellm-dashboard/src/components/budgets/edit_budget_modal.tsx +++ b/ui/litellm-dashboard/src/components/budgets/edit_budget_modal.tsx @@ -1,106 +1,217 @@ import React, { useEffect } from "react"; -import { TextInput, Accordion, AccordionHeader, AccordionBody } from "@tremor/react"; -import { Button as Button2, Modal, Form, InputNumber, Select } from "antd"; import { useUpdateBudget } from "@/app/(dashboard)/hooks/budgets/useBudgets"; import { budgetItem } from "./budget_panel"; import NotificationsManager from "../molecules/notifications_manager"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Controller, FormProvider, useForm } from "react-hook-form"; interface EditBudgetModalProps { isModalVisible: boolean; setIsModalVisible: React.Dispatch>; existingBudget: budgetItem; } + +type EditBudgetFormValues = { + budget_id: string; + tpm_limit: number | null; + rpm_limit: number | null; + max_budget: number | null; + budget_duration: string | null; +}; + +function LabeledRow({ + id, + label, + help, + children, + disabledHint, +}: { + id?: string; + label: string; + help?: string; + children: React.ReactNode; + disabledHint?: string; +}) { + return ( +
+ +
+ {children} + {help &&

{help}

} + {disabledHint && ( +

{disabledHint}

+ )} +
+
+ ); +} + const EditBudgetModal: React.FC = ({ isModalVisible, setIsModalVisible, existingBudget, }) => { - const [form] = Form.useForm(); + const form = useForm({ + defaultValues: { + budget_id: existingBudget.budget_id, + tpm_limit: existingBudget.tpm_limit, + rpm_limit: existingBudget.rpm_limit, + max_budget: existingBudget.max_budget, + budget_duration: null, + }, + mode: "onSubmit", + }); const updateBudget = useUpdateBudget(); useEffect(() => { - form.setFieldsValue(existingBudget); - }, [existingBudget, form]); - - const handleOk = () => { - setIsModalVisible(false); - form.resetFields(); - }; + form.reset({ + budget_id: existingBudget.budget_id, + tpm_limit: existingBudget.tpm_limit, + rpm_limit: existingBudget.rpm_limit, + max_budget: existingBudget.max_budget, + budget_duration: null, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [existingBudget]); const handleCancel = () => { setIsModalVisible(false); - form.resetFields(); }; - const handleUpdate = async (formValues: Record) => { + const handleUpdate = form.handleSubmit(async (values) => { try { NotificationsManager.info("Making API Call"); - await updateBudget.mutateAsync(formValues); + await updateBudget.mutateAsync(values); NotificationsManager.success("Budget Updated"); - form.resetFields(); setIsModalVisible(false); } catch (error) { console.error("Error updating the budget:", error); NotificationsManager.fromBackend(`Error updating the budget: ${error}`); } - }; + }); return ( - (!o ? handleCancel() : undefined)} > -
- <> - - - - - - - - - + + + Edit Budget + + Update an existing budget. Budget ID cannot be changed after creation. + + + + + + + + + + + + + - - - Optional Settings - - - - - - - - - - - + + + + Optional Settings + + + + + + + ( + + )} + /> + + + + -
- Save -
- -
+ + + + + + + + ); };