diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx index f5436223c7c..345e66079e1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx @@ -170,7 +170,7 @@ vi.mock("@/app/(dashboard)/hooks/useIsOrgAdmin", () => ({ })); vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({ - useOrganizations: vi.fn(() => ({ data: [], isPending: false })), + useOrganizations: vi.fn(() => ({ data: undefined, isPending: true, isLoading: false })), })); vi.mock("@/app/(dashboard)/hooks/users/useCurrentUser", () => ({ @@ -1495,7 +1495,9 @@ describe("UsagePage", () => { await waitFor(() => { expect(lastQuery()).toBe("?range=30d"); }); - expect(warning).toHaveBeenCalledWith("You don't have access to that usage view"); + expect(warning).toHaveBeenCalledWith("You don't have access to that usage view", { + id: "usage-url-access-denied", + }); expect(screen.queryByTestId("entity-usage")).not.toBeInTheDocument(); }); @@ -1508,7 +1510,9 @@ describe("UsagePage", () => { await waitFor(() => { expect(lastQuery()).toBe(""); }); - expect(warning).toHaveBeenCalledWith("You don't have access to that usage view"); + expect(warning).toHaveBeenCalledWith("You don't have access to that usage view", { + id: "usage-url-access-denied", + }); expect(mockUserDailyActivityAggregatedCall).not.toHaveBeenCalledWith( expect.anything(), expect.anything(), diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx index 956f7af9885..6d361f05fa5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx @@ -150,17 +150,18 @@ const UsagePage: React.FC = ({ teams, organizations }) => { // For non-admins or "my-usage" view, always pass their own user_id const effectiveUserId = usageView === "my-usage" || !isAdmin ? userID || null : urlParams.user; - const { isPending: organizationsPending } = useOrganizations(); + const { isLoading: organizationsLoading } = useOrganizations(); const allowedViews = useMemo( () => allowedUsageOptions(userRole, canViewTagUsage, isOrgAdmin), [userRole, canViewTagUsage, isOrgAdmin], ); - const accessSettled = userRole !== null && !organizationsPending; + const accessSettled = userRole !== null && !organizationsLoading; useEffect(() => { if (!accessSettled) return; const cleanup = cleanUsageUrl(urlParams, { allowedViews, isAdmin }); if (!cleanup) return; - if (cleanup.deniedAccess) toast.warning("You don't have access to that usage view"); + if (cleanup.deniedAccess) + toast.warning("You don't have access to that usage view", { id: "usage-url-access-denied" }); void setUrlParams(cleanup.patch); }, [accessSettled, urlParams, allowedViews, isAdmin, setUrlParams]); diff --git a/ui/litellm-dashboard/src/lib/toast.test.ts b/ui/litellm-dashboard/src/lib/toast.test.ts index 0afd0ec2cc4..b4a776f123a 100644 --- a/ui/litellm-dashboard/src/lib/toast.test.ts +++ b/ui/litellm-dashboard/src/lib/toast.test.ts @@ -37,6 +37,11 @@ describe("toast", () => { expect(sonner.success).toHaveBeenCalledWith("saved", { description: "Model x", duration: 1500 }); }); + it("passes an id through so repeated toasts collapse into one", () => { + toast.warning("no access", { id: "denied" }); + expect(sonner.warning).toHaveBeenCalledWith("no access", expect.objectContaining({ id: "denied" })); + }); + it("dismiss clears every toast", () => { toast.dismiss(); expect(sonner.dismiss).toHaveBeenCalledWith(); diff --git a/ui/litellm-dashboard/src/lib/toast.ts b/ui/litellm-dashboard/src/lib/toast.ts index a057d144e6f..a7a4ce45ad0 100644 --- a/ui/litellm-dashboard/src/lib/toast.ts +++ b/ui/litellm-dashboard/src/lib/toast.ts @@ -7,6 +7,7 @@ export type ToastKind = "success" | "info" | "warning" | "error"; export type ToastOptions = { readonly description?: ReactNode; readonly durationMs?: number; + readonly id?: string; }; type ErrorFacts = { @@ -127,6 +128,7 @@ const titleFor = ({ status, proxyType }: ErrorFacts): string => { const show = (kind: ToastKind, message: ReactNode, options?: ToastOptions): void => { sonner[kind](message, { + id: options?.id, description: options?.description, duration: options?.durationMs ?? DEFAULT_DURATION_MS[kind], });