mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 10:21:52 +00:00
Merge pull request #22416 from BerriAI/litellm_project_opt_in
[Feature] UI - Admin Settings: Projects Opt-In Toggle
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user