mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): migrate budgets to shadcn
- budget_panel.tsx: @tremor/react Table / Card / TabGroup / TabList / TabPanel / Button / Text \u2192 shadcn Table / Card / Tabs / Button + plain Tailwind for text. Moved action buttons into a proper TableCell (antd Table accepted dangling children but shadcn's semantic <table> element does not). - budget_modal.tsx: antd Modal / Form / InputNumber / Select + @tremor/react TextInput / Accordion \u2192 shadcn Dialog / react-hook-form / Input / Select / Accordion. Introduces a small LabeledRow helper for the label-on-left-value-on-right layout that antd's Form.Item with labelCol/wrapperCol used to provide. - edit_budget_modal.tsx: same treatment; preserves the disabled Budget ID field and the Accordion-wrapped optional settings. Gates: TS \u2713 | Lint \u2713 (pre-existing unused-imports errors in budget_panel.test.tsx unchanged) | Vitest 6/6 \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
b8d48fecca
commit
01cc55886e
4 changed files with 499 additions and 252 deletions
|
|
@ -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**
|
||||
|
||||
|
|
|
|||
|
|
@ -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<React.SetStateAction<boolean>>;
|
||||
}
|
||||
const BudgetModal: React.FC<BudgetModalProps> = ({ 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 (
|
||||
<div className="grid grid-cols-3 gap-3 items-start mb-4">
|
||||
<Label htmlFor={id} className="pt-2">
|
||||
{label}
|
||||
{required && <span className="text-destructive"> *</span>}
|
||||
</Label>
|
||||
<div className="col-span-2 space-y-1">
|
||||
{children}
|
||||
{help && <p className="text-xs text-muted-foreground">{help}</p>}
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BudgetModal: React.FC<BudgetModalProps> = ({
|
||||
isModalVisible,
|
||||
setIsModalVisible,
|
||||
}) => {
|
||||
const form = useForm<BudgetFormValues>({ defaultValues, mode: "onSubmit" });
|
||||
const createBudget = useCreateBudget();
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
form.reset(defaultValues);
|
||||
};
|
||||
|
||||
const handleCreate = async (formValues: Record<string, any>) => {
|
||||
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 (
|
||||
<Modal
|
||||
title="Create Budget"
|
||||
<Dialog
|
||||
open={isModalVisible}
|
||||
width={800}
|
||||
footer={null}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
onOpenChange={(o) => (!o ? handleCancel() : undefined)}
|
||||
>
|
||||
<Form form={form} onFinish={handleCreate} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
|
||||
<>
|
||||
<Form.Item
|
||||
label="Budget ID"
|
||||
name="budget_id"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please input a human-friendly name for the budget",
|
||||
},
|
||||
]}
|
||||
help="A human-friendly name for the budget"
|
||||
>
|
||||
<TextInput placeholder="" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Max Tokens per minute" name="tpm_limit" help="Default is model limit.">
|
||||
<InputNumber step={1} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Max Requests per minute" name="rpm_limit" help="Default is model limit.">
|
||||
<InputNumber step={1} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Budget</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Create a new spend / rate-limit budget.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleCreate}>
|
||||
<LabeledRow
|
||||
id="budget_id"
|
||||
label="Budget ID"
|
||||
help="A human-friendly name for the budget"
|
||||
required
|
||||
error={form.formState.errors.budget_id?.message as string | undefined}
|
||||
>
|
||||
<Input
|
||||
id="budget_id"
|
||||
{...form.register("budget_id", {
|
||||
required: "Please input a human-friendly name for the budget",
|
||||
})}
|
||||
/>
|
||||
</LabeledRow>
|
||||
<LabeledRow
|
||||
id="tpm_limit"
|
||||
label="Max Tokens per minute"
|
||||
help="Default is model limit."
|
||||
>
|
||||
<Input
|
||||
id="tpm_limit"
|
||||
type="number"
|
||||
step={1}
|
||||
{...form.register("tpm_limit", { valueAsNumber: true })}
|
||||
/>
|
||||
</LabeledRow>
|
||||
<LabeledRow
|
||||
id="rpm_limit"
|
||||
label="Max Requests per minute"
|
||||
help="Default is model limit."
|
||||
>
|
||||
<Input
|
||||
id="rpm_limit"
|
||||
type="number"
|
||||
step={1}
|
||||
{...form.register("rpm_limit", { valueAsNumber: true })}
|
||||
/>
|
||||
</LabeledRow>
|
||||
|
||||
<Accordion className="mt-20 mb-8">
|
||||
<AccordionHeader>
|
||||
<b>Optional Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||
<InputNumber step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item className="mt-8" label="Reset Budget" name="budget_duration">
|
||||
<Select defaultValue={null} placeholder="n/a">
|
||||
<Select.Option value="24h">daily</Select.Option>
|
||||
<Select.Option value="7d">weekly</Select.Option>
|
||||
<Select.Option value="30d">monthly</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</>
|
||||
<Accordion type="single" collapsible className="mt-8 mb-4">
|
||||
<AccordionItem value="optional">
|
||||
<AccordionTrigger className="font-semibold">
|
||||
Optional Settings
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<LabeledRow id="max_budget" label="Max Budget (USD)">
|
||||
<Input
|
||||
id="max_budget"
|
||||
type="number"
|
||||
step={0.01}
|
||||
{...form.register("max_budget", { valueAsNumber: true })}
|
||||
/>
|
||||
</LabeledRow>
|
||||
<LabeledRow label="Reset Budget">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="budget_duration"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={field.value ?? ""}
|
||||
onValueChange={(v) => field.onChange(v || null)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="n/a" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="24h">daily</SelectItem>
|
||||
<SelectItem value="7d">weekly</SelectItem>
|
||||
<SelectItem value="30d">monthly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</LabeledRow>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Create Budget</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
<DialogFooter className="mt-2">
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={createBudget.isPending}>
|
||||
{createBudget.isPending ? "Creating…" : "Create Budget"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<BudgetSettingsPageProps> = ({ 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<BudgetSettingsPageProps> = ({ 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<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleDeleteCancel = () => {
|
||||
setIsDeleteModalVisible(false);
|
||||
};
|
||||
const handleDeleteCancel = () => setIsDeleteModalVisible(false);
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
<Button size="sm" variant="primary" className="mb-2" onClick={() => setIsCreateModelVisible(true)}>
|
||||
<Button size="sm" className="mb-2" onClick={() => setIsCreateModelVisible(true)}>
|
||||
+ Create Budget
|
||||
</Button>
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab>Budgets</Tab>
|
||||
<Tab>Examples</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<div className="mt-6">
|
||||
<BudgetModal
|
||||
isModalVisible={isCreateModelVisible}
|
||||
setIsModalVisible={setIsCreateModelVisible}
|
||||
<Tabs defaultValue="budgets">
|
||||
<TabsList>
|
||||
<TabsTrigger value="budgets">Budgets</TabsTrigger>
|
||||
<TabsTrigger value="examples">Examples</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="budgets">
|
||||
<div className="mt-6">
|
||||
<BudgetModal
|
||||
isModalVisible={isCreateModelVisible}
|
||||
setIsModalVisible={setIsCreateModelVisible}
|
||||
/>
|
||||
{selectedBudget && (
|
||||
<EditBudgetModal
|
||||
isModalVisible={isEditModalVisible}
|
||||
setIsModalVisible={setIsEditModalVisible}
|
||||
existingBudget={selectedBudget}
|
||||
/>
|
||||
{selectedBudget && (
|
||||
<EditBudgetModal
|
||||
isModalVisible={isEditModalVisible}
|
||||
setIsModalVisible={setIsEditModalVisible}
|
||||
existingBudget={selectedBudget}
|
||||
/>
|
||||
)}
|
||||
<Card>
|
||||
<Text>Create a budget to assign to customers.</Text>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Budget ID</TableHeaderCell>
|
||||
<TableHeaderCell>Max Budget</TableHeaderCell>
|
||||
<TableHeaderCell>TPM</TableHeaderCell>
|
||||
<TableHeaderCell>RPM</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
)}
|
||||
<Card className="p-6">
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Create a budget to assign to customers.
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Budget ID</TableHead>
|
||||
<TableHead>Max Budget</TableHead>
|
||||
<TableHead>TPM</TableHead>
|
||||
<TableHead>RPM</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{budgetList
|
||||
.slice()
|
||||
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
|
||||
.map((value: budgetItem) => (
|
||||
<TableRow key={value.budget_id}>
|
||||
<TableCell>{value.budget_id}</TableCell>
|
||||
<TableCell>{value.max_budget ? value.max_budget : "n/a"}</TableCell>
|
||||
<TableCell>{value.tpm_limit ? value.tpm_limit : "n/a"}</TableCell>
|
||||
<TableCell>{value.rpm_limit ? value.rpm_limit : "n/a"}</TableCell>
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit budget"
|
||||
onClick={() => handleEditCall(value)}
|
||||
dataTestId="edit-budget-button"
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete budget"
|
||||
onClick={() => handleDeleteClick(value)}
|
||||
dataTestId="delete-budget-button"
|
||||
/>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalVisible}
|
||||
title="Delete Budget?"
|
||||
message="Are you sure you want to delete this budget? This action cannot be undone."
|
||||
resourceInformationTitle="Budget Information"
|
||||
resourceInformation={[
|
||||
{ label: "Budget ID", value: selectedBudget?.budget_id, code: true },
|
||||
{ label: "Max Budget", value: selectedBudget?.max_budget },
|
||||
{ label: "TPM", value: selectedBudget?.tpm_limit },
|
||||
{ label: "RPM", value: selectedBudget?.rpm_limit },
|
||||
]}
|
||||
onCancel={handleDeleteCancel}
|
||||
onOk={handleDeleteConfirm}
|
||||
confirmLoading={deleteBudget.isPending}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className="mt-6">
|
||||
<Text className="text-base">How to use budget id</Text>
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab>Assign Budget to Customer</Tab>
|
||||
<Tab>Test it (Curl)</Tab>
|
||||
<Tab>Test it (OpenAI SDK)</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<SyntaxHighlighter language="bash">{CREATE_END_USER_CURL_COMMAND}</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SyntaxHighlighter language="bash">{CHAT_COMPLETIONS_CURL_COMMAND}</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SyntaxHighlighter language="python">{OPENAI_SDK_PYTHON_CODE}</SyntaxHighlighter>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
<TableBody>
|
||||
{budgetList
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updated_at).getTime() -
|
||||
new Date(a.updated_at).getTime(),
|
||||
)
|
||||
.map((value: budgetItem) => (
|
||||
<TableRow key={value.budget_id}>
|
||||
<TableCell>{value.budget_id}</TableCell>
|
||||
<TableCell>
|
||||
{value.max_budget ? value.max_budget : "n/a"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{value.tpm_limit ? value.tpm_limit : "n/a"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{value.rpm_limit ? value.rpm_limit : "n/a"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit budget"
|
||||
onClick={() => handleEditCall(value)}
|
||||
dataTestId="edit-budget-button"
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete budget"
|
||||
onClick={() => handleDeleteClick(value)}
|
||||
dataTestId="delete-budget-button"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalVisible}
|
||||
title="Delete Budget?"
|
||||
message="Are you sure you want to delete this budget? This action cannot be undone."
|
||||
resourceInformationTitle="Budget Information"
|
||||
resourceInformation={[
|
||||
{ label: "Budget ID", value: selectedBudget?.budget_id, code: true },
|
||||
{ label: "Max Budget", value: selectedBudget?.max_budget },
|
||||
{ label: "TPM", value: selectedBudget?.tpm_limit },
|
||||
{ label: "RPM", value: selectedBudget?.rpm_limit },
|
||||
]}
|
||||
onCancel={handleDeleteCancel}
|
||||
onOk={handleDeleteConfirm}
|
||||
confirmLoading={deleteBudget.isPending}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="examples">
|
||||
<div className="mt-6">
|
||||
<p className="text-base font-medium">How to use budget id</p>
|
||||
<Tabs defaultValue="assign">
|
||||
<TabsList>
|
||||
<TabsTrigger value="assign">Assign Budget to Customer</TabsTrigger>
|
||||
<TabsTrigger value="curl">Test it (Curl)</TabsTrigger>
|
||||
<TabsTrigger value="openai">Test it (OpenAI SDK)</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="assign">
|
||||
<SyntaxHighlighter language="bash">
|
||||
{CREATE_END_USER_CURL_COMMAND}
|
||||
</SyntaxHighlighter>
|
||||
</TabsContent>
|
||||
<TabsContent value="curl">
|
||||
<SyntaxHighlighter language="bash">
|
||||
{CHAT_COMPLETIONS_CURL_COMMAND}
|
||||
</SyntaxHighlighter>
|
||||
</TabsContent>
|
||||
<TabsContent value="openai">
|
||||
<SyntaxHighlighter language="python">
|
||||
{OPENAI_SDK_PYTHON_CODE}
|
||||
</SyntaxHighlighter>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<React.SetStateAction<boolean>>;
|
||||
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 (
|
||||
<div className="grid grid-cols-3 gap-3 items-start mb-4">
|
||||
<Label htmlFor={id} className="pt-2">
|
||||
{label}
|
||||
</Label>
|
||||
<div className="col-span-2 space-y-1">
|
||||
{children}
|
||||
{help && <p className="text-xs text-muted-foreground">{help}</p>}
|
||||
{disabledHint && (
|
||||
<p className="text-xs text-muted-foreground">{disabledHint}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const EditBudgetModal: React.FC<EditBudgetModalProps> = ({
|
||||
isModalVisible,
|
||||
setIsModalVisible,
|
||||
existingBudget,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const form = useForm<EditBudgetFormValues>({
|
||||
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<string, any>) => {
|
||||
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 (
|
||||
<Modal
|
||||
title="Edit Budget"
|
||||
<Dialog
|
||||
open={isModalVisible}
|
||||
width={800}
|
||||
footer={null}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
onOpenChange={(o) => (!o ? handleCancel() : undefined)}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleUpdate}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
initialValues={existingBudget}
|
||||
>
|
||||
<>
|
||||
<Form.Item
|
||||
label="Budget ID"
|
||||
name="budget_id"
|
||||
help="Budget ID cannot be changed after creation"
|
||||
>
|
||||
<TextInput placeholder="" disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Max Tokens per minute" name="tpm_limit" help="Default is model limit.">
|
||||
<InputNumber step={1} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Max Requests per minute" name="rpm_limit" help="Default is model limit.">
|
||||
<InputNumber step={1} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Budget</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Update an existing budget. Budget ID cannot be changed after creation.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleUpdate}>
|
||||
<LabeledRow
|
||||
id="budget_id"
|
||||
label="Budget ID"
|
||||
help="Budget ID cannot be changed after creation"
|
||||
>
|
||||
<Input id="budget_id" {...form.register("budget_id")} disabled />
|
||||
</LabeledRow>
|
||||
<LabeledRow
|
||||
id="tpm_limit"
|
||||
label="Max Tokens per minute"
|
||||
help="Default is model limit."
|
||||
>
|
||||
<Input
|
||||
id="tpm_limit"
|
||||
type="number"
|
||||
step={1}
|
||||
{...form.register("tpm_limit", { valueAsNumber: true })}
|
||||
/>
|
||||
</LabeledRow>
|
||||
<LabeledRow
|
||||
id="rpm_limit"
|
||||
label="Max Requests per minute"
|
||||
help="Default is model limit."
|
||||
>
|
||||
<Input
|
||||
id="rpm_limit"
|
||||
type="number"
|
||||
step={1}
|
||||
{...form.register("rpm_limit", { valueAsNumber: true })}
|
||||
/>
|
||||
</LabeledRow>
|
||||
|
||||
<Accordion className="mt-20 mb-8">
|
||||
<AccordionHeader>
|
||||
<b>Optional Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||
<InputNumber step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item className="mt-8" label="Reset Budget" name="budget_duration">
|
||||
<Select defaultValue={null} placeholder="n/a">
|
||||
<Select.Option value="24h">daily</Select.Option>
|
||||
<Select.Option value="7d">weekly</Select.Option>
|
||||
<Select.Option value="30d">monthly</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</>
|
||||
<Accordion type="single" collapsible className="mt-8 mb-4">
|
||||
<AccordionItem value="optional">
|
||||
<AccordionTrigger className="font-semibold">
|
||||
Optional Settings
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<LabeledRow id="max_budget" label="Max Budget (USD)">
|
||||
<Input
|
||||
id="max_budget"
|
||||
type="number"
|
||||
step={0.01}
|
||||
{...form.register("max_budget", { valueAsNumber: true })}
|
||||
/>
|
||||
</LabeledRow>
|
||||
<LabeledRow label="Reset Budget">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="budget_duration"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={field.value ?? ""}
|
||||
onValueChange={(v) => field.onChange(v || null)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="n/a" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="24h">daily</SelectItem>
|
||||
<SelectItem value="7d">weekly</SelectItem>
|
||||
<SelectItem value="30d">monthly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</LabeledRow>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Save</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
<DialogFooter className="mt-2">
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={updateBudget.isPending}>
|
||||
{updateBudget.isPending ? "Saving…" : "Save"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue