diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 23c6aa7c09e..0d8c3bb7963 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbo", + "dev": "next dev --webpack", "build": "next build", "start": "next start", "lint": "next lint", diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.test.tsx index 3784680062c..393c9e4a619 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.test.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.test.tsx @@ -44,7 +44,10 @@ const renderWithForm = (props = {}) => { it("should default allow_all_keys switch to unchecked for new servers", async () => { renderWithForm(); await expandPanel(); - const toggle = screen.getByRole("switch"); + // Find the switch associated with "Allow All LiteLLM Keys" text + // The first switch in the component is for allow_all_keys + const switches = screen.getAllByRole("switch"); + const toggle = switches[0]; expect(toggle).toHaveAttribute("aria-checked", "false"); }); @@ -62,7 +65,10 @@ const renderWithForm = (props = {}) => { }); const user = await expandPanel(); - const toggle = screen.getByRole("switch"); + // Find the switch associated with "Allow All LiteLLM Keys" text + // The first switch in the component is for allow_all_keys + const switches = screen.getAllByRole("switch"); + const toggle = switches[0]; expect(toggle).toHaveAttribute("aria-checked", "true"); await user.click(toggle); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx index 4d80b383703..173b623a2ff 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx @@ -11,6 +11,10 @@ vi.mock("../networking", () => ({ fetchMCPServerHealth: vi.fn(), deleteMCPServer: vi.fn(), getProxyBaseUrl: vi.fn().mockReturnValue("http://localhost:4000"), + fetchMCPClientIp: vi.fn().mockResolvedValue(null), + getGeneralSettingsCall: vi.fn().mockResolvedValue([]), + updateConfigFieldSetting: vi.fn().mockResolvedValue(undefined), + deleteConfigFieldSetting: vi.fn().mockResolvedValue(undefined), })); // Mock NotificationsManager diff --git a/ui/litellm-dashboard/src/components/team/team_info.test.tsx b/ui/litellm-dashboard/src/components/team/team_info.test.tsx index 6c68188e37c..fa1b13fbd2d 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.test.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.test.tsx @@ -573,4 +573,63 @@ describe("TeamInfoView", () => { expect(teamNameElements.length).toBeGreaterThan(0); }); }); + + it("should display soft budget in settings view when present", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + soft_budget: 500.75, + max_budget: 1000, + }) + ); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByText("Team Settings")).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByText(/Soft Budget:/)).toBeInTheDocument(); + expect(screen.getByText(/\$500\.75/)).toBeInTheDocument(); + }); + }); + + it("should display soft budget alerting emails in settings view when present", async () => { + const user = userEvent.setup(); + vi.mocked(networking.teamInfoCall).mockResolvedValue( + createMockTeamData({ + metadata: { + soft_budget_alerting_emails: ["alert1@test.com", "alert2@test.com"], + }, + }) + ); + + renderWithProviders(); + + await waitFor(() => { + const teamNameElements = screen.queryAllByText("Test Team"); + expect(teamNameElements.length).toBeGreaterThan(0); + }); + + const settingsTab = screen.getByRole("tab", { name: "Settings" }); + await user.click(settingsTab); + + await waitFor(() => { + expect(screen.getByText("Team Settings")).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByText(/Soft Budget Alerting Emails:/)).toBeInTheDocument(); + expect(screen.getByText(/alert1@test\.com, alert2@test\.com/)).toBeInTheDocument(); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx index 34ff903c864..35f5b87e071 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.tsx @@ -85,6 +85,7 @@ export interface TeamData { tpm_limit: number | null; rpm_limit: number | null; max_budget: number | null; + soft_budget?: number | null; budget_duration: string | null; models: string[]; blocked: boolean; @@ -424,7 +425,10 @@ const TeamInfoView: React.FC = ({ let parsedMetadata = {}; try { - parsedMetadata = values.metadata ? JSON.parse(values.metadata) : {}; + const rawMetadata = values.metadata ? JSON.parse(values.metadata) : {}; + // Exclude soft_budget_alerting_emails from parsed metadata since it's handled separately + const { soft_budget_alerting_emails, ...rest } = rawMetadata; + parsedMetadata = rest; } catch (e) { NotificationsManager.fromBackend("Invalid JSON in metadata field"); return; @@ -457,12 +461,20 @@ const TeamInfoView: React.FC = ({ tpm_limit: sanitizeNumeric(values.tpm_limit), rpm_limit: sanitizeNumeric(values.rpm_limit), max_budget: values.max_budget, + soft_budget: sanitizeNumeric(values.soft_budget), budget_duration: values.budget_duration, metadata: { ...parsedMetadata, guardrails: values.guardrails || [], logging: values.logging_settings || [], disable_global_guardrails: values.disable_global_guardrails || false, + soft_budget_alerting_emails: + typeof values.soft_budget_alerting_emails === "string" + ? values.soft_budget_alerting_emails + .split(",") + .map((email: string) => email.trim()) + .filter((email: string) => email.length > 0) + : values.soft_budget_alerting_emails || [], ...(secretManagerSettings !== undefined ? { secret_manager_settings: secretManagerSettings } : {}), }, policies: values.policies || [], @@ -754,6 +766,7 @@ const TeamInfoView: React.FC = ({ tpm_limit: info.tpm_limit, rpm_limit: info.rpm_limit, max_budget: info.max_budget, + soft_budget: info.soft_budget, budget_duration: info.budget_duration, team_member_tpm_limit: info.team_member_budget_table?.tpm_limit, team_member_rpm_limit: info.team_member_budget_table?.rpm_limit, @@ -762,9 +775,13 @@ const TeamInfoView: React.FC = ({ guardrails: info.metadata?.guardrails || [], policies: info.policies || [], disable_global_guardrails: info.metadata?.disable_global_guardrails || false, + soft_budget_alerting_emails: + Array.isArray(info.metadata?.soft_budget_alerting_emails) + ? info.metadata.soft_budget_alerting_emails.join(", ") + : "", metadata: info.metadata ? JSON.stringify( - (({ logging, secret_manager_settings, ...rest }) => rest)(info.metadata), + (({ logging, secret_manager_settings, soft_budget_alerting_emails, ...rest }) => rest)(info.metadata), null, 2, ) @@ -821,6 +838,18 @@ const TeamInfoView: React.FC = ({ + + + + + + + + = ({ Max Budget:{" "} {info.max_budget !== null ? `$${formatNumberWithCommas(info.max_budget, 4)}` : "No Limit"} +
+ Soft Budget:{" "} + {info.soft_budget !== null && info.soft_budget !== undefined + ? `$${formatNumberWithCommas(info.soft_budget, 4)}` + : "No Limit"} +
Budget Reset: {info.budget_duration || "Never"}
+ {info.metadata?.soft_budget_alerting_emails && + Array.isArray(info.metadata.soft_budget_alerting_emails) && + info.metadata.soft_budget_alerting_emails.length > 0 && ( +
+ Soft Budget Alerting Emails: {info.metadata.soft_budget_alerting_emails.join(", ")} +
+ )}
diff --git a/ui/litellm-dashboard/tsconfig.json b/ui/litellm-dashboard/tsconfig.json index 5b0352feb98..d24bdd340f7 100644 --- a/ui/litellm-dashboard/tsconfig.json +++ b/ui/litellm-dashboard/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ {