From 7dd76bc4e3e2a86c2c5fb1957b19c7da86afdee2 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 11 Nov 2025 18:50:15 -0800 Subject: [PATCH] Usage indicator Near Limit Fix (#16504) --- .../src/components/usage_indicator.test.tsx | 86 +++++++++++++++++ .../src/components/usage_indicator.tsx | 96 ++++++++++++------- 2 files changed, 145 insertions(+), 37 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/usage_indicator.test.tsx diff --git a/ui/litellm-dashboard/src/components/usage_indicator.test.tsx b/ui/litellm-dashboard/src/components/usage_indicator.test.tsx new file mode 100644 index 0000000000..5d82d5f154 --- /dev/null +++ b/ui/litellm-dashboard/src/components/usage_indicator.test.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { describe, it, expect, vi } from "vitest"; +import { render, waitFor } from "@testing-library/react"; +import UsageIndicator from "./usage_indicator"; + +vi.mock("./networking", () => { + return { + getRemainingUsers: vi.fn(), + }; +}); + +import { getRemainingUsers } from "./networking"; + +describe("UsageIndicator", () => { + it("does not show Near limit when users usage is below 80% (1/100 -> 1%)", async () => { + (getRemainingUsers as unknown as ReturnType).mockResolvedValue({ + total_users: 100, + total_users_used: 1, + total_users_remaining: 99, + total_teams: null, + total_teams_used: 0, + total_teams_remaining: null, + }); + + const { queryByText, findByText } = render(); + + await findByText("Usage"); + + expect(queryByText("Near limit")).toBeNull(); + }); + + it("handles null totals shape by rendering nothing", async () => { + (getRemainingUsers as unknown as ReturnType).mockResolvedValue({ + total_users: null, + total_teams: null, + total_users_used: 520, + total_teams_used: 4, + total_teams_remaining: null, + total_users_remaining: null, + }); + + const { container } = render(); + + await waitFor(() => { + expect(container.firstChild).toBeNull(); + }); + }); + + it("shows Near limit for Teams at 80% usage (4/5)", async () => { + (getRemainingUsers as unknown as ReturnType).mockResolvedValue({ + total_users: null, + total_users_used: 0, + total_users_remaining: null, + total_teams: 5, + total_teams_used: 4, + total_teams_remaining: 1, + }); + + const { findByText, getByText } = render(); + + await findByText("Usage"); + + // Teams section should show Near limit indicator + expect(getByText("Teams")).toBeTruthy(); + expect(getByText("Near limit")).toBeTruthy(); + }); + + it("shows Over limit for Users when usage exceeds 100% (105/100)", async () => { + (getRemainingUsers as unknown as ReturnType).mockResolvedValue({ + total_users: 100, + total_users_used: 105, + total_users_remaining: -5, + total_teams: null, + total_teams_used: 0, + total_teams_remaining: null, + }); + + const { findByText, getByText } = render(); + + await findByText("Usage"); + + // Users section should show Over limit indicator + expect(getByText("Users")).toBeTruthy(); + expect(getByText("Over limit")).toBeTruthy(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/usage_indicator.tsx b/ui/litellm-dashboard/src/components/usage_indicator.tsx index 9a3128f544..ed5e8e0755 100644 --- a/ui/litellm-dashboard/src/components/usage_indicator.tsx +++ b/ui/litellm-dashboard/src/components/usage_indicator.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from "react"; import { Badge } from "@tremor/react"; -import { AlertTriangle, Users, TrendingUp, Loader2, ChevronDown, ChevronUp, Minus, UserCheck } from "lucide-react"; +import { AlertTriangle, ChevronDown, ChevronUp, Loader2, Minus, TrendingUp, UserCheck, Users } from "lucide-react"; +import { useEffect, useState } from "react"; import { getRemainingUsers } from "./networking"; // Simple utility function to combine class names @@ -72,18 +72,14 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica } // User metrics - const userIsOverLimit = data.total_users_remaining ? data.total_users_remaining <= 0 : false; - const userIsNearLimit = data.total_users_remaining - ? data.total_users_remaining <= 5 && data.total_users_remaining > 0 - : false; const userUsagePercentage = data.total_users ? (data.total_users_used / data.total_users) * 100 : 0; + const userIsOverLimit = userUsagePercentage > 100; + const userIsNearLimit = userUsagePercentage >= 80 && userUsagePercentage <= 100; // Team metrics - const teamIsOverLimit = data.total_teams_remaining ? data.total_teams_remaining <= 0 : false; - const teamIsNearLimit = data.total_teams_remaining - ? data.total_teams_remaining <= 5 && data.total_teams_remaining > 0 - : false; const teamUsagePercentage = data.total_teams ? (data.total_teams_used / data.total_teams) * 100 : 0; + const teamIsOverLimit = teamUsagePercentage > 100; + const teamIsNearLimit = teamUsagePercentage >= 80 && teamUsagePercentage <= 100; // Combined status (worst case scenario) const isOverLimit = userIsOverLimit || teamIsOverLimit; @@ -115,12 +111,6 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica return "green"; }; - const getStatusText = () => { - if (isOverLimit) return "Over Limit"; - if (isNearLimit) return "Near Limit"; - return "Active"; - }; - const getStatusIcon = () => { if (isOverLimit) return ; if (isNearLimit) return ; @@ -338,8 +328,6 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica onClick={() => setIsMinimized(false)} className={cn( "bg-white border border-gray-200 rounded-lg shadow-sm p-3 hover:shadow-md transition-all w-full", - hasIssues && isOverLimit && "border-red-200 bg-red-50", - hasIssues && isNearLimit && "border-yellow-200 bg-yellow-50", )} title="Show usage details" > @@ -348,12 +336,26 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica {hasIssues && {getStatusIcon()}}
{data && data.total_users !== null && ( - + U: {data.total_users_used}/{data.total_users} )} {data && data.total_teams !== null && ( - + T: {data.total_teams_used}/{data.total_teams} )} @@ -396,23 +398,11 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica } return ( -
- {/* Header with title and minimize button */} +
Usage - {(isOverLimit || isNearLimit) && ( - - {getStatusText()} - - )}