diff --git a/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/BudgetTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/BudgetTable.tsx index 88e40dfba2f..80872c58d28 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/BudgetTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/BudgetTable.tsx @@ -226,6 +226,7 @@ const BudgetTable: React.FC = ({ list, canModify, onEditClick, columns={columns} getRowId={(budget, index) => budget.budget_id || String(index)} defaultColumnVisibility={BUDGET_TABLE_HIDDEN_COLUMNS} + fillHeight sortingMode="server" sorting={list.sorting} onSortingChange={list.onSortingChange} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx index fbcbd501313..e49f0ffc722 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx @@ -7,6 +7,7 @@ import { Plus, Wallet } from "lucide-react"; import React, { useCallback, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { PageHeader } from "@/components/shared/PageHeader"; +import { ToolbarSeparator } from "@/components/shared/ToolbarSeparator"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; @@ -78,31 +79,34 @@ const BudgetPanel: React.FC = ({ accessToken }) => { }; return ( -
+
} title="Budgets" subtitle="Spend, TPM and RPM limits you can assign to customers." /> - {canModify && ( -
- + +
+ {canModify && ( + <> + + + + )} + + + Budgets + + + Examples + +
- )} - - - - Budgets - - - Examples - - - -
+ +
{selectedBudget && ( = ({ accessToken }) => { />
- -
+ +

How to use budget id

diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx index 60547415908..8555a10c326 100644 --- a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx +++ b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx @@ -625,6 +625,44 @@ describe("DataTable layout", () => { const scroller = container.querySelector('[data-slot="table-container"]')?.parentElement as HTMLElement; expect(scroller.style.maxHeight).toBe("240px"); }); + + it("caps fillHeight at the parent's height instead of stretching to it, so a short table stays short", () => { + const { container } = render(); + const scroller = container.querySelector('[data-slot="table-container"]')?.parentElement as HTMLElement; + const frame = scroller.parentElement as HTMLElement; + const outer = frame.parentElement as HTMLElement; + + // A ceiling, not a stretch: flex-1 here would hold the footer at the bottom on a two-row table. + expect(outer.className).toContain("max-h-full"); + expect(outer.className).not.toContain("flex-1"); + expect(frame.className).not.toContain("flex-1"); + expect(scroller.className).not.toContain("flex-1"); + + expect(outer.className).toContain("flex-col"); + expect(frame.className).toContain("flex-col"); + expect(scroller.className).toContain("min-h-0"); + expect(scroller.className).toContain("overflow-auto"); + expect(scroller.style.maxHeight).toBe(""); + // Without this the Table primitive's own overflow container captures the sticky header. + expect(scroller.className).toContain("[&_[data-slot=table-container]]:overflow-visible"); + + const thead = container.querySelector("thead") as HTMLElement; + expect(thead.className).toContain("sticky"); + // Rows pass under the header, so the semi-transparent row tint alone would let them show through. + expect(thead.className).toContain("bg-background"); + }); + + it("leaves the default layout untouched when neither height mode is set", () => { + const { container } = render(); + const scroller = container.querySelector('[data-slot="table-container"]')?.parentElement as HTMLElement; + + expect(scroller.className).toContain("overflow-x-auto"); + expect(scroller.className).not.toContain("min-h-0"); + expect(scroller.style.maxHeight).toBe(""); + expect((scroller.parentElement as HTMLElement).className).not.toContain("flex-col"); + expect(container.querySelector("thead")?.className).not.toContain("sticky"); + expect(container.querySelector("thead")?.className).not.toContain("bg-background"); + }); }); describe("DataTable misconfiguration guards", () => { diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx index c4799594465..8cd0e25dfc4 100644 --- a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx +++ b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx @@ -48,6 +48,22 @@ const INTERACTIVE_SELECTOR = "button, a, input, select, textarea, [role=checkbox const noop = () => {}; +/** + * Height-filling mode. The table still sizes to its rows; the parent's height is only a ceiling, so + * a short table keeps its footer under the last row and a long one scrolls its rows instead of the + * page. `table-container` is the Table primitive's own overflow-x wrapper; left as a scroll box it + * captures the sticky header and the header scrolls away with the rows. And rows pass under that + * header, which the semi-transparent header row tint alone would not hide. + */ +const FILL_CLASSES = { + outer: "flex max-h-full min-h-0 flex-col", + frame: "flex min-h-0 flex-col", + body: "min-h-0 [&_[data-slot=table-container]]:overflow-visible", + header: "bg-background", +} as const; + +const NO_FILL_CLASSES = { outer: "", frame: "", body: "", header: "" } as const; + export class DataTableConfigError extends Error { constructor(messages: readonly string[]) { super(`DataTable misconfiguration:\n- ${messages.join("\n- ")}`); @@ -538,6 +554,7 @@ export function DataTable(props: DataTableProps(props: DataTableProps { @@ -604,15 +622,15 @@ export function DataTable(props: DataTableProps -
- {toolbar !== undefined &&
{toolbar(table)}
} +
+
+ {toolbar !== undefined &&
{toolbar(table)}
}
- + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( @@ -631,7 +649,7 @@ export function DataTable(props: DataTableProps{footer(table)}}
- {paginationNode !== null &&
{paginationNode}
} + {paginationNode !== null &&
{paginationNode}
}
); diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/types.ts b/ui/litellm-dashboard/src/components/shared/DataTable/types.ts index 40f3a4df204..dd578f4df45 100644 --- a/ui/litellm-dashboard/src/components/shared/DataTable/types.ts +++ b/ui/litellm-dashboard/src/components/shared/DataTable/types.ts @@ -69,6 +69,12 @@ export interface DataTableProps { rowClassName?: (row: Row) => string; maxBodyHeight?: number | string; + /** + * Scroll the rows inside whatever height the parent gives the table, rather than growing the page. + * The table becomes a flex column, so the parent must be a height-constrained flex container; without + * one it degrades to the normal auto-height layout. Use instead of `maxBodyHeight` to avoid a magic number. + */ + fillHeight?: boolean; size?: DataTableSize; toolbar?: (table: Table) => React.ReactNode;