diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 74cff0315a..ceda08d520 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -99,6 +99,11 @@ class UISettings(BaseModel): description="If enabled, forwards client headers (e.g. Authorization) to the LLM API. Required for Claude Code with Max subscription.", ) + enable_projects_ui: bool = Field( + default=False, + description="If enabled, shows the Projects feature in the UI sidebar and the project field in key management.", + ) + class UISettingsResponse(SettingsResponse): """Response model for UI settings""" @@ -113,6 +118,7 @@ ALLOWED_UI_SETTINGS_FIELDS = { "enabled_ui_pages_internal_users", "require_auth_for_public_ai_hub", "forward_client_headers_to_llm_api", + "enable_projects_ui", } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx index 26f5786b41..17f62a20f7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx @@ -14,6 +14,7 @@ interface SidebarProviderProps { const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: SidebarProviderProps) => { const { accessToken } = useAuthorized(); const [enabledPagesInternalUsers, setEnabledPagesInternalUsers] = useState(null); + const [enableProjectsUI, setEnableProjectsUI] = useState(false); useEffect(() => { const fetchUISettings = async () => { @@ -34,6 +35,10 @@ const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: Side } else { console.log("[SidebarProvider] No enabled_ui_pages_internal_users in response (all pages visible by default)"); } + + if (settings?.values?.enable_projects_ui !== undefined) { + setEnableProjectsUI(Boolean(settings.values.enable_projects_ui)); + } } catch (error) { console.error("[SidebarProvider] Failed to fetch UI settings:", error); } @@ -48,6 +53,7 @@ const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: Side defaultSelectedKey={defaultSelectedKey} collapsed={sidebarCollapsed} enabledPagesInternalUsers={enabledPagesInternalUsers} + enableProjectsUI={enableProjectsUI} /> ); }; diff --git a/ui/litellm-dashboard/src/components/AdminPanel.tsx b/ui/litellm-dashboard/src/components/AdminPanel.tsx index 82bb08794f..1d69606ff2 100644 --- a/ui/litellm-dashboard/src/components/AdminPanel.tsx +++ b/ui/litellm-dashboard/src/components/AdminPanel.tsx @@ -14,16 +14,12 @@ import { TableHeaderCell, TableRow, } from "@tremor/react"; -import { Alert, Button as Button2, Form, Input, Modal, Tabs, Typography } from "antd"; +import { Alert, Button as Button2, Form, Input, Modal, Space, Tabs, Typography } from "antd"; import React, { useEffect, useState } from "react"; +import NewBadge from "./common_components/NewBadge"; import { useBaseUrl } from "./constants"; import NotificationsManager from "./molecules/notifications_manager"; -import { - addAllowedIP, - deleteAllowedIP, - getAllowedIPs, - getSSOSettings, -} from "./networking"; +import { addAllowedIP, deleteAllowedIP, getAllowedIPs, getSSOSettings } from "./networking"; import SCIMConfig from "./SCIM"; import SSOSettings from "./Settings/AdminSettings/SSOSettings/SSOSettings"; import UISettings from "./Settings/AdminSettings/UISettings/UISettings"; @@ -356,7 +352,13 @@ const AdminPanel: React.FC = ({ proxySettings }) => { }, { key: "ui-settings", - label: "UI Settings", + label: ( + + + UI Settings + + + ), children: , }, ]; diff --git a/ui/litellm-dashboard/src/components/Projects/ProjectsPage.tsx b/ui/litellm-dashboard/src/components/Projects/ProjectsPage.tsx index 9a41b63d01..3762ddbb37 100644 --- a/ui/litellm-dashboard/src/components/Projects/ProjectsPage.tsx +++ b/ui/litellm-dashboard/src/components/Projects/ProjectsPage.tsx @@ -198,7 +198,7 @@ export function ProjectsPage() { > - Projects + [BETA] Projects Manage projects within your teams diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx index d99053d0a4..5d99dd2969 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx @@ -17,6 +17,7 @@ export default function UISettings() { const disableTeamAdminDeleteProperty = schema?.properties?.disable_team_admin_delete_team_user; const requireAuthForPublicAIHubProperty = schema?.properties?.require_auth_for_public_ai_hub; const forwardClientHeadersProperty = schema?.properties?.forward_client_headers_to_llm_api; + const enableProjectsUIProperty = schema?.properties?.enable_projects_ui; const enabledPagesProperty = schema?.properties?.enabled_ui_pages_internal_users; const values = data?.values ?? {}; const isDisabledForInternalUsers = Boolean(values.disable_model_add_for_internal_users); @@ -75,6 +76,21 @@ export default function UISettings() { ); }; + const handleToggleEnableProjectsUI = (checked: boolean) => { + updateSettings( + { enable_projects_ui: checked }, + { + onSuccess: () => { + NotificationManager.success("UI settings updated successfully. Refreshing page..."); + setTimeout(() => window.location.reload(), 1000); + }, + onError: (error) => { + NotificationManager.fromBackend(error); + }, + }, + ); + }; + const handleToggleRequireAuthForPublicAIHub = (checked: boolean) => { updateSettings( { require_auth_for_public_ai_hub: checked }, @@ -176,6 +192,23 @@ export default function UISettings() { + + + + [BETA] Enable Projects (page will refresh) + + {enableProjectsUIProperty?.description ?? + "If enabled, shows the Projects feature in the UI sidebar and the project field in key management."} + + + + {/* Page Visibility for Internal Users */} diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index bb0bd54c7a..e84b6e86e4 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -41,6 +41,7 @@ interface SidebarProps { defaultSelectedKey: string; collapsed?: boolean; enabledPagesInternalUsers?: string[] | null; + enableProjectsUI?: boolean; } // Menu item configuration @@ -300,7 +301,11 @@ const menuGroups: MenuGroup[] = [ { key: "settings", page: "settings", - label: Settings, + label: ( + + Settings + + ), icon: , roles: all_admin_roles, children: [ @@ -321,7 +326,11 @@ const menuGroups: MenuGroup[] = [ { key: "admin-panel", page: "admin-panel", - label: "Admin Settings", + label: ( + + Admin Settings + + ), icon: , roles: all_admin_roles, }, @@ -345,7 +354,7 @@ const menuGroups: MenuGroup[] = [ }, ]; -const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapsed = false, enabledPagesInternalUsers }) => { +const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapsed = false, enabledPagesInternalUsers, enableProjectsUI }) => { const { userId, accessToken, userRole } = useAuthorized(); const { data: organizations } = useOrganizations(); @@ -398,6 +407,9 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse return true; } + // Hide Projects page if enableProjectsUI is not enabled + if (item.key === "projects" && !enableProjectsUI) return false; + // Existing role check if (item.roles && !item.roles.includes(userRole)) return false; diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index 97260bf2ad..802dfd14b6 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -1,6 +1,7 @@ "use client"; import { keyKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects"; +import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { InfoCircleOutlined } from "@ant-design/icons"; @@ -146,6 +147,8 @@ export const fetchUserModels = async ( const CreateKey: React.FC = ({ team, teams, data, addKey }) => { const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized(); const { data: projects, isLoading: isProjectsLoading } = useProjects(); + const { data: uiSettingsData } = useUISettings(); + const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui); const queryClient = useQueryClient(); const [form] = Form.useForm(); const [isModalVisible, setIsModalVisible] = useState(false); @@ -692,33 +695,35 @@ const CreateKey: React.FC = ({ team, teams, data, addKey }) => { }} /> - - Project{" "} - - - - - } - name="project_id" - className="mt-4" - > - { - if (!projectId) { - setSelectedProjectId(null); - setSelectedCreateKeyTeam(null); - form.setFieldValue("team_id", undefined); - return; - } - setSelectedProjectId(projectId); - }} - /> - + {enableProjectsUI && ( + + Project{" "} + + + + + } + name="project_id" + className="mt-4" + > + { + if (!projectId) { + setSelectedProjectId(null); + setSelectedCreateKeyTeam(null); + form.setFieldValue("team_id", undefined); + return; + } + setSelectedProjectId(projectId); + }} + /> + + )} {/* Show message when team selection is required */} diff --git a/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx b/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx index 00cf234393..0d88083c3f 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyInfoView.handleKeyUpdate.test.tsx @@ -254,6 +254,11 @@ vi.mock("@/app/(dashboard)/hooks/projects/useProjects", () => ({ useProjects: vi.fn().mockReturnValue({ data: [], isLoading: false }), })); +// Mock useUISettings hook +vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({ + useUISettings: vi.fn().mockReturnValue({ data: { values: {} }, isLoading: false }), +})); + // KeyEditView mock: triggers onSubmit with our injected form values vi.mock("./key_edit_view", async () => { const React = await import("react"); diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx index 4923b71f76..8a8a018f26 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx @@ -1,5 +1,6 @@ import GuardrailSelector from "@/components/guardrails/GuardrailSelector"; import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects"; +import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; import PolicySelector from "@/components/policies/PolicySelector"; import { InfoCircleOutlined } from "@ant-design/icons"; import { TextInput, Button as TremorButton } from "@tremor/react"; @@ -97,6 +98,8 @@ export function KeyEditView({ const [rotationInterval, setRotationInterval] = useState(keyData.rotation_interval || ""); const [isKeySaving, setIsKeySaving] = useState(false); const { data: projects } = useProjects(); + const { data: uiSettingsData } = useUISettings(); + const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui); const hasProject = Boolean(keyData.project_id); const projectDisplay = (() => { if (!keyData.project_id) return null; @@ -603,12 +606,12 @@ export function KeyEditView({ - {hasProject && ( + {enableProjectsUI && hasProject && ( diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index 9aac652a58..72895c4d61 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -1,5 +1,6 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects"; +import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; import useTeams from "@/app/(dashboard)/hooks/useTeams"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils"; @@ -50,6 +51,8 @@ export default function KeyInfoView({ const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized(); const { teams: teamsData } = useTeams(); const { data: projects } = useProjects(); + const { data: uiSettingsData } = useUISettings(); + const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui); const [isEditing, setIsEditing] = useState(false); const [form] = Form.useForm(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -573,19 +576,21 @@ export default function KeyInfoView({ {currentKeyData.team_id || "Not Set"} -
- Project - - {currentKeyData.project_id - ? (() => { - const project = projects?.find((p) => p.project_id === currentKeyData.project_id); - return project?.project_alias - ? `${project.project_alias} (${currentKeyData.project_id})` - : currentKeyData.project_id; - })() - : "Not Set"} - -
+ {enableProjectsUI && ( +
+ Project + + {currentKeyData.project_id + ? (() => { + const project = projects?.find((p) => p.project_id === currentKeyData.project_id); + return project?.project_alias + ? `${project.project_alias} (${currentKeyData.project_id})` + : currentKeyData.project_id; + })() + : "Not Set"} + +
+ )}
Organization