mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
feat(ui): surface team budget on key overview when key has no own budget (#30801)
* feat(ui): surface team budget on key overview when key has no own budget * fix(ui): replace IIFE with derived variable and use find() for team budget display
This commit is contained in:
parent
d7205918b5
commit
6bffdebd1d
3 changed files with 95 additions and 11 deletions
|
|
@ -467,10 +467,15 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
|
|||
enableSorting: true,
|
||||
cell: (info) => {
|
||||
const maxBudget = info.getValue() as number | null;
|
||||
if (maxBudget === null) {
|
||||
return "Unlimited";
|
||||
if (maxBudget !== null) {
|
||||
return `$${formatNumberWithCommas(maxBudget)}`;
|
||||
}
|
||||
return `$${formatNumberWithCommas(maxBudget)}`;
|
||||
const teamId = info.row.original.team_id;
|
||||
const team = teams?.find((t) => t.team_id === teamId);
|
||||
if (team?.max_budget != null) {
|
||||
return `$${formatNumberWithCommas(team.max_budget)} (Team)`;
|
||||
}
|
||||
return "Unlimited";
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { KeyResponse } from "../key_team_helpers/key_list";
|
||||
import { KeyResponse, Team } from "../key_team_helpers/key_list";
|
||||
import KeyInfoView from "./key_info_view";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import useTeams from "@/app/(dashboard)/hooks/useTeams";
|
||||
|
|
@ -103,8 +103,26 @@ const baseAuthorized = {
|
|||
userEmail: null,
|
||||
disabledPersonalKeyCreation: null,
|
||||
showSSOBanner: false,
|
||||
isLoading: false,
|
||||
isAuthorized: true,
|
||||
};
|
||||
|
||||
const makeTeam = (overrides: Partial<Team>): Team => ({
|
||||
team_id: "team-default",
|
||||
team_alias: "Default Team",
|
||||
models: [],
|
||||
max_budget: null,
|
||||
budget_duration: null,
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
organization_id: "",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
keys: [],
|
||||
members_with_roles: [],
|
||||
spend: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("KeyInfoView overview budget display (LIT-2845)", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useTeams).mockReturnValue({ teams: [], setTeams: vi.fn() });
|
||||
|
|
@ -151,7 +169,64 @@ describe("KeyInfoView overview budget display (LIT-2845)", () => {
|
|||
it("renders 'Unlimited' when max_budget is null", async () => {
|
||||
renderWithProviders(
|
||||
<KeyInfoView
|
||||
keyData={{ ...MOCK_KEY_DATA, max_budget: null }}
|
||||
keyData={{ ...MOCK_KEY_DATA, max_budget: null } as unknown as KeyResponse}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
teams={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/of Unlimited/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders team budget with alias and duration when key has no own budget but team has one", async () => {
|
||||
vi.mocked(useTeams).mockReturnValue({
|
||||
teams: [makeTeam({ team_id: "team-123", team_alias: "Test Budget", max_budget: 1200, budget_duration: "30d" })],
|
||||
setTeams: vi.fn(),
|
||||
});
|
||||
renderWithProviders(
|
||||
<KeyInfoView
|
||||
keyData={{ ...MOCK_KEY_DATA, max_budget: null, team_id: "team-123" } as unknown as KeyResponse}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
teams={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/of \$1,200\.00 \(Team: Test Budget \/ 30d\)/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders team budget without duration when team has no budget_duration", async () => {
|
||||
vi.mocked(useTeams).mockReturnValue({
|
||||
teams: [makeTeam({ team_id: "team-456", team_alias: "No Duration Team", max_budget: 500 })],
|
||||
setTeams: vi.fn(),
|
||||
});
|
||||
renderWithProviders(
|
||||
<KeyInfoView
|
||||
keyData={{ ...MOCK_KEY_DATA, max_budget: null, team_id: "team-456" } as unknown as KeyResponse}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
teams={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/of \$500\.00 \(Team: No Duration Team\)/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders 'Unlimited' when key has no budget and team also has no budget", async () => {
|
||||
vi.mocked(useTeams).mockReturnValue({
|
||||
teams: [makeTeam({ team_id: "team-789", team_alias: "Free Team" })],
|
||||
setTeams: vi.fn(),
|
||||
});
|
||||
renderWithProviders(
|
||||
<KeyInfoView
|
||||
keyData={{ ...MOCK_KEY_DATA, max_budget: null, team_id: "team-789" } as unknown as KeyResponse}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
|
|
|
|||
|
|
@ -411,6 +411,15 @@ export default function KeyInfoView({
|
|||
});
|
||||
};
|
||||
|
||||
const parentTeam = currentKeyData.team_id ? teamsData?.find((team) => team.team_id === currentKeyData.team_id) : null;
|
||||
|
||||
const budgetDisplay =
|
||||
currentKeyData.max_budget !== null
|
||||
? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}`
|
||||
: parentTeam?.max_budget != null
|
||||
? `$${formatNumberWithCommas(parentTeam.max_budget, 2)} (Team: ${parentTeam.team_alias || parentTeam.team_id}${parentTeam.budget_duration ? ` / ${parentTeam.budget_duration}` : ""})`
|
||||
: "Unlimited";
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-y-auto p-4">
|
||||
<KeyInfoHeader
|
||||
|
|
@ -520,12 +529,7 @@ export default function KeyInfoView({
|
|||
<Text>Spend</Text>
|
||||
<div className="mt-2">
|
||||
<Title>${formatNumberWithCommas(currentKeyData.spend, 4)}</Title>
|
||||
<Text>
|
||||
of{" "}
|
||||
{currentKeyData.max_budget !== null
|
||||
? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}`
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
<Text>of {budgetDisplay}</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue