diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.activity.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.activity.test.tsx index ca7adf07941..2b0e65d9fd9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.activity.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.activity.test.tsx @@ -1,5 +1,13 @@ -import { fireEvent, render, waitFor } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { fireEvent, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { renderWithProviders, testQueryClient } from "../../../../../tests/test-utils"; + +const { useAuthorizedMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn() })); + +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: useAuthorizedMock, +})); const mockUserDailyActivityCall = vi.fn(); @@ -36,10 +44,15 @@ const singlePage = { }; describe("CostOptimizationView daily activity", () => { + beforeEach(() => { + testQueryClient.clear(); + useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole: "proxy_admin" }); + }); + it("fetches daily activity once for the page and shares it with every tab that needs it", async () => { mockUserDailyActivityCall.mockResolvedValue(singlePage); - const { getByRole, getByTestId } = render( + const { getByRole, getByTestId } = renderWithProviders( , ); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx index c6d5a410418..ab3744a0383 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx @@ -1,6 +1,8 @@ -import { fireEvent, render } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { renderWithProviders, testQueryClient } from "../../../../../tests/test-utils"; + const { useAuthorizedMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn() })); vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ @@ -19,11 +21,12 @@ import CostOptimizationView from "./CostOptimizationView"; const renderView = (userRole = "Admin") => { useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole }); - return render(); + return renderWithProviders(); }; describe("CostOptimizationView", () => { beforeEach(() => { + testQueryClient.clear(); useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole: "Admin" }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.test.tsx index ad68111bba7..45d8b15bfb9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.test.tsx @@ -1,10 +1,11 @@ -import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ToolSpendResponse } from "@/components/networking"; import type { DailyData, SpendMetrics } from "@/components/UsagePage/types"; +import { renderWithProviders, testQueryClient } from "../../../../../tests/test-utils"; + const mockGetToolSpend = vi.fn(); const { useAuthorizedMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn() })); @@ -106,7 +107,7 @@ const renderWith = (results: DailyData[], options: RenderOptions = {}) => { } = options; mockGetToolSpend.mockResolvedValue(toolSpend); useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole }); - return render( + return renderWithProviders( JSON.parse(element.getAttribute("da describe("UsageTab", () => { beforeEach(() => { + testQueryClient.clear(); mockGetToolSpend.mockReset(); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts index 734c1986f8f..dc757d175af 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/organizations/useOrganizations.ts @@ -10,7 +10,14 @@ export interface OrganizationListFilters { org_alias?: string | null; } -export const useOrganizations = (filters?: OrganizationListFilters): UseQueryResult => { +export interface UseOrganizationsOptions { + enabled?: boolean; +} + +export const useOrganizations = ( + filters?: OrganizationListFilters, + options?: UseOrganizationsOptions, +): UseQueryResult => { const { accessToken, userId, userRole } = useAuthorized(); const orgId = filters?.org_id || null; const orgAlias = filters?.org_alias || null; @@ -21,7 +28,7 @@ export const useOrganizations = (filters?: OrganizationListFilters): UseQueryRes : {}, ), queryFn: async () => await organizationListCall(accessToken!, orgId, orgAlias), - enabled: Boolean(accessToken && userId && userRole), + enabled: (options?.enabled ?? true) && Boolean(accessToken && userId && userRole), }); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useCan.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useCan.ts index 13903007cae..3d7788432b8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useCan.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useCan.ts @@ -1,13 +1,13 @@ "use client"; -import { hasCapability, type Capability } from "@/utils/capabilities"; +import { capabilityRequiresOrgAdmin, hasCapability, type Capability } from "@/utils/capabilities"; import useAuthorized from "./useAuthorized"; import useIsOrgAdmin from "./useIsOrgAdmin"; const useCan = (capability: Capability): boolean => { const { userRole } = useAuthorized(); - const isOrgAdmin = useIsOrgAdmin(); + const isOrgAdmin = useIsOrgAdmin({ enabled: capabilityRequiresOrgAdmin(capability) }); return hasCapability(userRole, capability, isOrgAdmin); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useIsOrgAdmin.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useIsOrgAdmin.ts index d93b57a3fc3..61bc68ca250 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useIsOrgAdmin.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useIsOrgAdmin.ts @@ -5,9 +5,16 @@ import { isOrgAdminForAnyOrg, isOrgAdminSessionRole } from "@/utils/roles"; import { useOrganizations } from "./organizations/useOrganizations"; import useAuthorized from "./useAuthorized"; -const useIsOrgAdmin = (): boolean => { +export interface UseIsOrgAdminOptions { + enabled?: boolean; +} + +const useIsOrgAdmin = (options?: UseIsOrgAdminOptions): boolean => { const { userId, userRole } = useAuthorized(); - const { data: organizations } = useOrganizations(); + const { data: organizations } = useOrganizations(undefined, options); + if (options?.enabled === false) { + return isOrgAdminSessionRole(userRole); + } return isOrgAdminSessionRole(userRole) || isOrgAdminForAnyOrg(organizations, userId); }; diff --git a/ui/litellm-dashboard/src/utils/capabilities.ts b/ui/litellm-dashboard/src/utils/capabilities.ts index dabbc827bcb..abb130bd255 100644 --- a/ui/litellm-dashboard/src/utils/capabilities.ts +++ b/ui/litellm-dashboard/src/utils/capabilities.ts @@ -21,6 +21,8 @@ export type Capability = keyof typeof CAPABILITY_ROLES; const ORG_ADMIN_CAPABILITIES: ReadonlySet = new Set(["viewDeletedTeams"]); +export const capabilityRequiresOrgAdmin = (capability: Capability): boolean => ORG_ADMIN_CAPABILITIES.has(capability); + export const hasCapability = ( userRole: string | null | undefined, capability: Capability,