Merge pull request #22416 from BerriAI/litellm_project_opt_in

[Feature] UI - Admin Settings: Projects Opt-In Toggle
This commit is contained in:
yuneng-jiang
2026-02-28 10:42:01 -08:00
committed by GitHub
10 changed files with 132 additions and 55 deletions
@@ -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",
}
@@ -14,6 +14,7 @@ interface SidebarProviderProps {
const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: SidebarProviderProps) => {
const { accessToken } = useAuthorized();
const [enabledPagesInternalUsers, setEnabledPagesInternalUsers] = useState<string[] | null>(null);
const [enableProjectsUI, setEnableProjectsUI] = useState<boolean>(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}
/>
);
};
@@ -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<AdminPanelProps> = ({ proxySettings }) => {
},
{
key: "ui-settings",
label: "UI Settings",
label: (
<Space>
<Text>
UI Settings <NewBadge />
</Text>
</Space>
),
children: <UISettings />,
},
];
@@ -198,7 +198,7 @@ export function ProjectsPage() {
>
<Space direction="vertical" size={0}>
<Title level={2} style={{ margin: 0 }}>
Projects
[BETA] Projects
</Title>
<Text type="secondary">
Manage projects within your teams
@@ -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() {
</Space>
</Space>
<Space align="start" size="middle">
<Switch
checked={Boolean(values.enable_projects_ui)}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleEnableProjectsUI}
aria-label={enableProjectsUIProperty?.description ?? "Enable Projects UI"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>[BETA] Enable Projects (page will refresh)</Typography.Text>
<Typography.Text type="secondary">
{enableProjectsUIProperty?.description ??
"If enabled, shows the Projects feature in the UI sidebar and the project field in key management."}
</Typography.Text>
</Space>
</Space>
<Divider />
{/* Page Visibility for Internal Users */}
@@ -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: <span className="flex items-center gap-4">Settings</span>,
label: (
<span className="flex items-center gap-2">
Settings <NewBadge />
</span>
),
icon: <SettingOutlined />,
roles: all_admin_roles,
children: [
@@ -321,7 +326,11 @@ const menuGroups: MenuGroup[] = [
{
key: "admin-panel",
page: "admin-panel",
label: "Admin Settings",
label: (
<span className="flex items-center gap-2">
Admin Settings <NewBadge dot><span /></NewBadge>
</span>
),
icon: <SettingOutlined />,
roles: all_admin_roles,
},
@@ -345,7 +354,7 @@ const menuGroups: MenuGroup[] = [
},
];
const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapsed = false, enabledPagesInternalUsers }) => {
const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapsed = false, enabledPagesInternalUsers, enableProjectsUI }) => {
const { userId, accessToken, userRole } = useAuthorized();
const { data: organizations } = useOrganizations();
@@ -398,6 +407,9 @@ const Sidebar: React.FC<SidebarProps> = ({ 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;
@@ -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<CreateKeyProps> = ({ 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<CreateKeyProps> = ({ team, teams, data, addKey }) => {
}}
/>
</Form.Item>
<Form.Item
label={
<span>
Project{" "}
<Tooltip title="Assign this key to a project. Selecting a project will lock the team to the project's team.">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="project_id"
className="mt-4"
>
<ProjectDropdown
projects={projects}
teamId={selectedCreateKeyTeam?.team_id}
loading={isProjectsLoading || !teams}
onChange={(projectId) => {
if (!projectId) {
setSelectedProjectId(null);
setSelectedCreateKeyTeam(null);
form.setFieldValue("team_id", undefined);
return;
}
setSelectedProjectId(projectId);
}}
/>
</Form.Item>
{enableProjectsUI && (
<Form.Item
label={
<span>
Project{" "}
<Tooltip title="Assign this key to a project. Selecting a project will lock the team to the project's team.">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="project_id"
className="mt-4"
>
<ProjectDropdown
projects={projects}
teamId={selectedCreateKeyTeam?.team_id}
loading={isProjectsLoading || !teams}
onChange={(projectId) => {
if (!projectId) {
setSelectedProjectId(null);
setSelectedCreateKeyTeam(null);
form.setFieldValue("team_id", undefined);
return;
}
setSelectedProjectId(projectId);
}}
/>
</Form.Item>
)}
</div>
{/* Show message when team selection is required */}
@@ -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");
@@ -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<string>(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({
<Form.Item
label="Team ID"
name="team_id"
help={hasProject ? "Team is locked because this key belongs to a project" : undefined}
help={enableProjectsUI && hasProject ? "Team is locked because this key belongs to a project" : undefined}
>
<Select
placeholder="Select team"
showSearch
disabled={hasProject}
disabled={enableProjectsUI && hasProject}
style={{ width: "100%" }}
filterOption={(input, option) => {
const team = teams?.find((t) => t.team_id === option?.value);
@@ -623,7 +626,7 @@ export function KeyEditView({
))}
</Select>
</Form.Item>
{hasProject && (
{enableProjectsUI && hasProject && (
<Form.Item label="Project">
<Input value={projectDisplay ?? ""} disabled />
</Form.Item>
@@ -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({
<Text>{currentKeyData.team_id || "Not Set"}</Text>
</div>
<div>
<Text className="font-medium">Project</Text>
<Text>
{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"}
</Text>
</div>
{enableProjectsUI && (
<div>
<Text className="font-medium">Project</Text>
<Text>
{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"}
</Text>
</div>
)}
<div>
<Text className="font-medium">Organization</Text>