diff --git a/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py index 7e272a2c59c..97ab91a46ac 100644 --- a/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/management_endpoints/project_endpoints.py @@ -1178,7 +1178,9 @@ async def _resolve_project_daily_activity_scope( found_by_id: Final = {p.project_id: p for p in projects} missing: Final = [pid for pid in requested if pid not in found_by_id] if missing: - raise _project_daily_activity_error(status_code=404, message=f"Project(s) not found: {missing}") + raise _project_daily_activity_error( + status_code=404, message=f"Project(s) not found: {', '.join(sorted(missing))}" + ) if not user_api_key_has_admin_view(user_api_key_dict): for project_id in requested: diff --git a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py index 61a66d46e5c..1f7221fd2dd 100644 --- a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py +++ b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py @@ -1517,11 +1517,12 @@ async def test_get_project_daily_activity_unknown_project_404(monkeypatch): with pytest.raises(HTTPException) as exc_info: await get_project_daily_activity( user_api_key_dict=admin, - project_ids="does-not-exist", + project_ids="does-not-exist,also-missing", start_date="2026-09-01", end_date="2026-09-02", ) assert exc_info.value.status_code == 404 + assert exc_info.value.detail == {"error": "Project(s) not found: also-missing, does-not-exist"} mock_prisma.db.query_raw.assert_not_called() diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiSettings/useUISettings.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiSettings/useUISettings.ts index 749fc98c0d8..eaa1c89eab4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiSettings/useUISettings.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiSettings/useUISettings.ts @@ -17,7 +17,7 @@ export const useUISettings = (options?: { staleTime?: number; refetchInterval?: return useQuery>({ queryKey: uiSettingsKeys.list({}), queryFn: async () => await getUiSettings(), - staleTime: options?.staleTime ?? 60 * 60 * 1000, // 1 hour - data rarely changes + staleTime: options?.staleTime ?? 60 * 60 * 1000, gcTime: 60 * 60 * 1000, // 1 hour - keep in cache for 1 hour refetchInterval: options?.refetchInterval, }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx index 273e478528e..f55dc05f5c2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx @@ -10,16 +10,15 @@ import { type ProviderSpendRow, } from "./entityUsageAggregations"; import { buildCostBreakdownTiles, buildSummaryTiles, hasFlatCost, type SummaryTile } from "./entityUsageSummary"; +import { SummaryTileCard } from "./SummaryTileCard"; import { MoneyCell } from "@/components/shared/table_cells"; import { Card as ShadcnCard, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { hasCapability, type Capability } from "@/utils/capabilities"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import type { DateRangePickerValue } from "@/components/shared/date_picker_types"; -import { ChevronDown, ChevronRight, Info } from "lucide-react"; import type { ColumnDef } from "@tanstack/react-table"; import PaginationStatusAlerts from "@/components/shared/PaginationStatusAlerts"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import React, { type ReactNode, useMemo, useState } from "react"; import TeamMultiSelect from "@/components/common_components/team_multi_select"; import UserDropdown from "@/components/common_components/UserDropdown"; @@ -358,29 +357,13 @@ const EntityUsage: React.FC = ({ [], ); - const chev = "size-3 text-muted-foreground"; - const expandIcon = showCostBreakdown ? : ; - - const renderSummaryTile = ({ title, value, className, tooltip, expandable }: SummaryTile) => ( - setShowCostBreakdown(!showCostBreakdown) : undefined} - > - -
-

{title}

- {tooltip ? ( - - } /> - {tooltip} - - ) : null} - {expandable ? expandIcon : null} -
-

{value}

-
-
+ const renderSummaryTile = (tile: SummaryTile) => ( + setShowCostBreakdown(!showCostBreakdown)} + /> ); const breakdownTiles = showFlatCost && showCostBreakdown ? buildCostBreakdownTiles(spendData.metadata) : []; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/SummaryTileCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/SummaryTileCard.tsx new file mode 100644 index 00000000000..fcf616bca99 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/SummaryTileCard.tsx @@ -0,0 +1,41 @@ +import { ChevronDown, ChevronRight, Info } from "lucide-react"; + +import { Card as ShadcnCard, CardContent } from "@/components/ui/card"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; + +import type { SummaryTile } from "./entityUsageSummary"; + +interface SummaryTileCardProps { + tile: SummaryTile; + expanded?: boolean; + onToggleExpand?: () => void; +} + +export function SummaryTileCard({ tile, expanded = false, onToggleExpand }: SummaryTileCardProps) { + const { title, value, className, tooltip, expandable } = tile; + const chev = "size-3 text-muted-foreground"; + const expandIcon = expanded ? : ; + + return ( + + +
+

{title}

+ {tooltip ? ( + + } /> + {tooltip} + + ) : null} + {expandable ? expandIcon : null} +
+

{value}

+
+
+ ); +} + +export default SummaryTileCard; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.test.tsx index 3d5a17fe411..7ea78355188 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.test.tsx @@ -73,12 +73,9 @@ describe("ProjectUsage", () => { await user.click(screen.getByRole("option", { name: "Project Alpha" })); await waitFor(() => expect(mockProjectDailyActivityCall).toHaveBeenCalledTimes(1)); - expect(mockProjectDailyActivityCall).toHaveBeenCalledWith( - "test-token", - DATE_VALUE.from, - DATE_VALUE.to, - ["project-alpha"], - ); + expect(mockProjectDailyActivityCall).toHaveBeenCalledWith("test-token", DATE_VALUE.from, DATE_VALUE.to, [ + "project-alpha", + ]); await waitFor(() => expect(screen.getAllByText("$12.50").length).toBeGreaterThan(0)); expect(screen.getAllByText("4").length).toBeGreaterThan(0); @@ -101,6 +98,20 @@ describe("ProjectUsage", () => { expect(screen.queryByText("No project usage data")).not.toBeInTheDocument(); }); + it("renders a backend not-found message verbatim, without mangling the comma-joined list", async () => { + mockProjectDailyActivityCall.mockRejectedValue(new Error("Project(s) not found: also-missing, does-not-exist")); + + const user = userEvent.setup(); + renderWithProviders( + , + ); + + await user.click(screen.getByRole("combobox")); + await user.click(screen.getByRole("option", { name: "Project Alpha" })); + + expect(await screen.findByText("Project(s) not found: also-missing, does-not-exist")).toBeInTheDocument(); + }); + it("shows a loading indicator while fetching data for a newly-added project", async () => { let resolveSecondCall: (value: unknown) => void = () => {}; mockProjectDailyActivityCall diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.tsx index 62e3d675318..5e48d05ecf6 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.tsx @@ -13,15 +13,11 @@ import { projectDailyActivityCall } from "@/components/networking"; import { extractProxyErrorMessage } from "@/lib/http/client"; import { valueFormatterSpend } from "@/components/UsagePage/utils/value_formatters"; -import { buildSummaryTiles, type SummaryTile } from "../EntityUsage/entityUsageSummary"; +import { buildSummaryTiles } from "../EntityUsage/entityUsageSummary"; +import { SummaryTileCard } from "../EntityUsage/SummaryTileCard"; import type { EntityList } from "../EntityUsage/EntityUsage"; import ProjectSpendBreakdown from "./ProjectSpendBreakdown"; -import { - buildDailySpendSeries, - buildProjectSpendBreakdown, - humanizeBackendListMessage, - summarizeProjectUsage, -} from "./projectUsageAggregations"; +import { buildDailySpendSeries, buildProjectSpendBreakdown, summarizeProjectUsage } from "./projectUsageAggregations"; interface ProjectUsageProps { accessToken: string | null; @@ -30,15 +26,6 @@ interface ProjectUsageProps { premiumUser: boolean; } -const renderSummaryTile = ({ title, value, className }: SummaryTile) => ( - - -

{title}

-

{value}

-
-
-); - const ProjectUsage: React.FC = ({ accessToken, projectList, dateValue, premiumUser }) => { const [selectedProjectIds, setSelectedProjectIds] = useState([]); @@ -57,12 +44,18 @@ const ProjectUsage: React.FC = ({ accessToken, projectList, d const queryOptions = { queryKey: ["project-daily-activity", selectedProjectIds, startTime?.toISOString(), endTime?.toISOString()], - queryFn: () => projectDailyActivityCall(accessToken as string, startTime as Date, endTime as Date, selectedProjectIds), + queryFn: () => + projectDailyActivityCall(accessToken as string, startTime as Date, endTime as Date, selectedProjectIds), enabled, placeholderData: keepPreviousData, }; const { data, isPending, isFetching, isPlaceholderData, isError, error } = useQuery(queryOptions); + const rows = useMemo(() => data?.results ?? [], [data]); + const summary = useMemo(() => summarizeProjectUsage(rows), [rows]); + const dailySpend = useMemo(() => buildDailySpendSeries(rows), [rows]); + const projectBreakdown = useMemo(() => buildProjectSpendBreakdown(rows), [rows]); + if (!premiumUser) { return ( @@ -78,10 +71,6 @@ const ProjectUsage: React.FC = ({ accessToken, projectList, d ); } - const rows = data?.results ?? []; - const summary = summarizeProjectUsage(rows); - const dailySpend = buildDailySpendSeries(rows); - const projectBreakdown = buildProjectSpendBreakdown(rows); const isLoadingRows = isPending || (isFetching && isPlaceholderData); const renderResultsPanel = () => { @@ -104,7 +93,7 @@ const ProjectUsage: React.FC = ({ accessToken, projectList, d
Could not load project usage - {humanizeBackendListMessage(extractProxyErrorMessage(error))} + {extractProxyErrorMessage(error)}
); @@ -120,7 +109,9 @@ const ProjectUsage: React.FC = ({ accessToken, projectList, d ) : (
- {buildSummaryTiles(summary, false).map(renderSummaryTile)} + {buildSummaryTiles(summary, false).map((tile) => ( + + ))}
)} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts index c95747f7a7f..7ef733f5eac 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts @@ -2,12 +2,7 @@ import { describe, expect, it } from "vitest"; import type { ProjectDailySpendRow } from "@/components/networking"; -import { - buildDailySpendSeries, - buildProjectSpendBreakdown, - humanizeBackendListMessage, - summarizeProjectUsage, -} from "./projectUsageAggregations"; +import { buildDailySpendSeries, buildProjectSpendBreakdown, summarizeProjectUsage } from "./projectUsageAggregations"; const row = (overrides: Partial = {}): ProjectDailySpendRow => ({ date: "2026-09-01", @@ -126,23 +121,3 @@ describe("buildProjectSpendBreakdown", () => { expect(buildProjectSpendBreakdown(rows)[0].project_alias).toBe("Project Alpha"); }); }); - -describe("humanizeBackendListMessage", () => { - it("strips a single-quoted Python list down to plain text", () => { - expect(humanizeBackendListMessage("Project(s) not found: ['proj-123']")).toBe( - "Project(s) not found: proj-123", - ); - }); - - it("comma-joins a multi-item Python list", () => { - expect(humanizeBackendListMessage("Project(s) not found: ['proj-1', 'proj-2']")).toBe( - "Project(s) not found: proj-1, proj-2", - ); - }); - - it("leaves a message with no trailing list untouched", () => { - expect(humanizeBackendListMessage("Not authorized to view this project")).toBe( - "Not authorized to view this project", - ); - }); -}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts index a94b3aa67e4..ed385c56344 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts @@ -44,20 +44,27 @@ export const summarizeProjectUsage = (rows: ProjectDailySpendRow[]): ProjectUsag ); export const buildDailySpendSeries = (rows: ProjectDailySpendRow[]): DailyProjectSpendPoint[] => { - const spendByDate = rows.reduce>( - (totals, row) => ({ ...totals, [row.date]: (totals[row.date] ?? 0) + row.spend }), - {}, - ); - return Object.entries(spendByDate) + const spendByDate = new Map(); + for (const row of rows) { + spendByDate.set(row.date, (spendByDate.get(row.date) ?? 0) + row.spend); + } + return [...spendByDate.entries()] .map(([date, spend]) => ({ date, spend })) .sort((a, b) => a.date.localeCompare(b.date)); }; -const groupByProjectId = (rows: ProjectDailySpendRow[]): Record => - rows.reduce>( - (groups, row) => ({ ...groups, [row.project_id]: [...(groups[row.project_id] ?? []), row] }), - {}, - ); +const groupByProjectId = (rows: ProjectDailySpendRow[]): ProjectDailySpendRow[][] => { + const groups = new Map(); + for (const row of rows) { + const existing = groups.get(row.project_id); + if (existing) { + existing.push(row); + } else { + groups.set(row.project_id, [row]); + } + } + return [...groups.values()]; +}; const summarizeProjectGroup = (rows: ProjectDailySpendRow[]): ProjectSpendRow => { const [{ project_id, project_alias }] = rows; @@ -75,22 +82,18 @@ const summarizeProjectGroup = (rows: ProjectDailySpendRow[]): ProjectSpendRow => }; const disambiguateAliases = (rows: ProjectSpendRow[]): ProjectSpendRow[] => { - const aliasCounts = rows.reduce>( - (counts, row) => ({ ...counts, [row.project_alias]: (counts[row.project_alias] ?? 0) + 1 }), - {}, - ); + const aliasCounts = new Map(); + for (const row of rows) { + aliasCounts.set(row.project_alias, (aliasCounts.get(row.project_alias) ?? 0) + 1); + } return rows.map((row) => - aliasCounts[row.project_alias] > 1 ? { ...row, project_alias: `${row.project_alias} (${row.project_id})` } : row, + (aliasCounts.get(row.project_alias) ?? 0) > 1 + ? { ...row, project_alias: `${row.project_alias} (${row.project_id})` } + : row, ); }; export const buildProjectSpendBreakdown = (rows: ProjectDailySpendRow[]): ProjectSpendRow[] => { - const summarized = Object.values(groupByProjectId(rows)).map(summarizeProjectGroup); + const summarized = groupByProjectId(rows).map(summarizeProjectGroup); return disambiguateAliases(summarized).sort((a, b) => b.spend - a.spend); }; - -export const humanizeBackendListMessage = (message: string): string => - message.replace(/\[([^\]]*)]\s*$/, (_match, listContents: string) => { - const items = [...listContents.matchAll(/'([^']*)'|"([^"]*)"/g)].map((m) => m[1] ?? m[2]); - return items.length > 0 ? items.join(", ") : listContents; - }); 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 b6ca7397677..b841d3f0a3b 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 @@ -24,6 +24,7 @@ import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import useIsOrgAdmin from "@/app/(dashboard)/hooks/useIsOrgAdmin"; import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser"; +import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; import { hasCapability } from "@/utils/capabilities"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { all_admin_roles, internalUserRoles } from "@/utils/roles"; @@ -111,7 +112,9 @@ const UsagePage: React.FC = ({ teams, organizations }) => { const isOrgAdmin = useIsOrgAdmin(); const canViewOrganizationUsage = hasCapability(userRole, "viewOrganizationUsage", isOrgAdmin); const canViewAgentUsage = hasCapability(userRole, "viewAgentUsage"); - const canViewProjectUsage = hasCapability(userRole, "viewProjectUsage"); + const { data: uiSettingsData } = useUISettings(); + const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui); + const canViewProjectUsage = hasCapability(userRole, "viewProjectUsage") && enableProjectsUI; // For admins: null means global view (all users), a string means filter by that user // For non-admins: always set to their own user ID @@ -484,6 +487,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { userRole={userRole} canViewTagUsage={canViewTagUsage} isOrgAdmin={isOrgAdmin} + enableProjectsUI={enableProjectsUI} /> @@ -937,7 +941,6 @@ const UsagePage: React.FC = ({ teams, organizations }) => { /> )} - {/* Project Usage Panel */} {usageView === "project" && canViewProjectUsage && ( { expect(offers(container, optionName)).toBe(expected); }); + it("should hide Project Usage from an admin when enableProjectsUI is false", async () => { + const user = userEvent.setup(); + const { container } = render( + , + ); + + await openMenu(user); + expect(offers(container, "Project Usage")).toBe(false); + }); + it.each(["Team Usage", "Tag Usage"])("should keep %s available to an internal user", async (optionName) => { const user = userEvent.setup(); const { container } = render( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx index a79267860ea..6cf7cfb1140 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx @@ -21,6 +21,7 @@ export interface UsageViewSelectProps { userRole: string | null; canViewTagUsage?: boolean; isOrgAdmin?: boolean; + enableProjectsUI?: boolean; title?: string; description?: string; "data-id"?: string; @@ -118,6 +119,7 @@ export const UsageViewSelect: React.FC = ({ userRole, canViewTagUsage = false, isOrgAdmin = false, + enableProjectsUI = true, title = "Usage View", description = "Select the usage data you want to view", "data-id": dataId, @@ -125,6 +127,9 @@ export const UsageViewSelect: React.FC = ({ const isAdmin = all_admin_roles.includes(userRole ?? ""); const getFilteredOptions = () => { return OPTIONS.filter((option) => { + if (option.value === "project" && !enableProjectsUI) { + return false; + } if (option.capability) { return hasCapability(userRole, option.capability, isOrgAdmin); } diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.test.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.test.tsx index fd7df329f33..56c5b612707 100644 --- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.test.tsx @@ -28,11 +28,16 @@ vi.mock("@/app/(dashboard)/hooks/projects/useProjects", () => ({ useProjects: vi.fn(), })); +vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({ + useUISettings: vi.fn(), +})); + import { useInfiniteSpendLogEndUsers } from "@/app/(dashboard)/hooks/spendLogs/useSpendLogEndUsers"; import { useInfiniteSpendLogUsers } from "@/app/(dashboard)/hooks/spendLogs/useSpendLogUsers"; import { useInfiniteKeyAliases } from "@/app/(dashboard)/hooks/keys/useKeyAliases"; import { useInfiniteModelInfo } from "@/app/(dashboard)/hooks/models/useModels"; import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects"; +import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; const emptyInfiniteQuery = { data: { pages: [], pageParams: [] }, @@ -85,6 +90,9 @@ describe("RequestLogsFilters", () => { vi.mocked(useProjects).mockReturnValue({ data: [], isLoading: false } as unknown as ReturnType< typeof useProjects >); + vi.mocked(useUISettings).mockReturnValue({ + data: { values: { enable_projects_ui: true } }, + } as unknown as ReturnType); }); it("renders every backend-supported filter field", async () => { @@ -152,6 +160,17 @@ describe("RequestLogsFilters", () => { expect(set).toHaveBeenCalledWith(LOG_FILTER_IDS.PROJECT_ID, "project-1"); }); + it("hides the Project filter when the projects UI setting is disabled", async () => { + vi.mocked(useUISettings).mockReturnValue({ + data: { values: { enable_projects_ui: false } }, + } as unknown as ReturnType); + + renderFilters(); + + expect(await screen.findByText("Team ID")).toBeInTheDocument(); + expect(screen.queryByText("Project")).not.toBeInTheDocument(); + }); + it("pushes the User ID picker query to the paginated user lookup", async () => { const user = userEvent.setup(); renderFilters(); diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.tsx index 9203edb8276..fe3e34bccdc 100644 --- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.tsx @@ -7,6 +7,7 @@ import { useInfiniteSpendLogUsers } from "@/app/(dashboard)/hooks/spendLogs/useS import { useInfiniteKeyAliases } from "@/app/(dashboard)/hooks/keys/useKeyAliases"; import { useInfiniteModelInfo } from "@/app/(dashboard)/hooks/models/useModels"; import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects"; +import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; import { DataTableFilterField } from "@/components/shared/DataTable"; import { PaginatedSearchSelect } from "@/components/shared/PaginatedSearchSelect"; import { SearchSelect, type SearchSelectOption } from "@/components/shared/SearchSelect"; @@ -346,6 +347,8 @@ interface RequestLogsFiltersProps { export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsFiltersProps) { const valueOf = (id: string): string => asString(get(id)); const setter = (id: string) => (next: string | undefined) => set(id, next); + const { data: uiSettingsData } = useUISettings(); + const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui); return ( <> @@ -355,7 +358,9 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF teams={teams} /> - + {enableProjectsUI && ( + + )}