mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
feat(ui): add admin flag to disable in-product UI nudges for everyone
Admins can now suppress the survey and Claude Code feedback popups for all users via a single disable_ui_nudges UI setting, instead of relying on each user dismissing them individually.
This commit is contained in:
parent
a4f57032e0
commit
96d819ee7e
4 changed files with 90 additions and 3 deletions
|
|
@ -172,6 +172,11 @@ class UISettings(BaseModel):
|
|||
description="If true, org admins cannot generate API keys via /key/generate.",
|
||||
)
|
||||
|
||||
disable_ui_nudges: bool = Field(
|
||||
default=False,
|
||||
description="If true, suppresses in-product UI nudges (survey and Claude Code feedback popups) for all users.",
|
||||
)
|
||||
|
||||
|
||||
class UISettingsResponse(SettingsResponse):
|
||||
"""Response model for UI settings"""
|
||||
|
|
@ -195,6 +200,7 @@ ALLOWED_UI_SETTINGS_FIELDS = {
|
|||
"scope_user_search_to_org",
|
||||
"disable_custom_api_keys",
|
||||
"disable_key_generate_for_org_admin",
|
||||
"disable_ui_nudges",
|
||||
}
|
||||
|
||||
# Flags that must be synced from the persisted UISettings into
|
||||
|
|
|
|||
|
|
@ -1032,6 +1032,45 @@ class TestProxySettingEndpoints:
|
|||
stored_settings = json.loads(create_data["ui_settings"])
|
||||
assert stored_settings["disable_model_add_for_internal_users"] is True
|
||||
|
||||
def test_update_ui_settings_persists_disable_ui_nudges(
|
||||
self, mock_auth, monkeypatch
|
||||
):
|
||||
"""disable_ui_nudges must be allowlisted so admins can suppress UI popups for everyone"""
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
|
||||
mock_user_auth = UserAPIKeyAuth(
|
||||
user_id="test-user-123",
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN,
|
||||
)
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: mock_user_auth
|
||||
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
|
||||
mock_prisma = MagicMock()
|
||||
mock_prisma.db.litellm_uisettings.upsert = AsyncMock()
|
||||
mock_prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
|
||||
|
||||
try:
|
||||
response = client.patch(
|
||||
"/update/ui_settings", json={"disable_ui_nudges": True}
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "success"
|
||||
assert data["settings"]["disable_ui_nudges"] is True
|
||||
|
||||
create_data = mock_prisma.db.litellm_uisettings.upsert.call_args.kwargs["data"][
|
||||
"create"
|
||||
]
|
||||
stored_settings = json.loads(create_data["ui_settings"])
|
||||
assert stored_settings["disable_ui_nudges"] is True
|
||||
|
||||
def test_update_ui_settings_ignores_non_allowlisted_value(
|
||||
self, mock_auth, monkeypatch
|
||||
):
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import BudgetPanel from "@/components/budgets/budget_panel";
|
|||
import CacheDashboard from "@/components/cache_dashboard";
|
||||
import ClaudeCodePluginsPanel from "@/components/claude_code_plugins";
|
||||
import { teamListCall as v2TeamListCall } from "@/app/(dashboard)/hooks/teams/useTeams";
|
||||
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
|
||||
import LoadingScreen from "@/components/common_components/LoadingScreen";
|
||||
import { CostTrackingSettings } from "@/components/CostTrackingSettings";
|
||||
import GeneralSettings from "@/components/general_settings";
|
||||
|
|
@ -90,6 +91,9 @@ function CreateKeyPageContent() {
|
|||
const [modelData, setModelData] = useState<any>({ data: [] });
|
||||
const [createClicked, setCreateClicked] = useState<boolean>(false);
|
||||
|
||||
const { data: uiSettingsData } = useUISettings();
|
||||
const nudgesDisabled = Boolean(uiSettingsData?.values?.disable_ui_nudges);
|
||||
|
||||
// Survey state - always show by default
|
||||
const [showSurveyPrompt, setShowSurveyPrompt] = useState(true);
|
||||
const [showSurveyModal, setShowSurveyModal] = useState(false);
|
||||
|
|
@ -265,6 +269,9 @@ function CreateKeyPageContent() {
|
|||
|
||||
// Fetch in-product nudges configuration from backend
|
||||
useEffect(() => {
|
||||
if (nudgesDisabled) {
|
||||
return;
|
||||
}
|
||||
if (accessToken && token) {
|
||||
(async () => {
|
||||
try {
|
||||
|
|
@ -284,7 +291,7 @@ function CreateKeyPageContent() {
|
|||
}
|
||||
})();
|
||||
}
|
||||
}, [accessToken, token]);
|
||||
}, [accessToken, token, nudgesDisabled]);
|
||||
|
||||
// Auto-dismiss survey prompt after 15 seconds
|
||||
useEffect(() => {
|
||||
|
|
@ -550,7 +557,7 @@ function CreateKeyPageContent() {
|
|||
|
||||
{/* Survey Components */}
|
||||
<SurveyPrompt
|
||||
isVisible={showSurveyPrompt}
|
||||
isVisible={showSurveyPrompt && !nudgesDisabled}
|
||||
onOpen={handleOpenSurvey}
|
||||
onDismiss={handleDismissSurveyPrompt}
|
||||
/>
|
||||
|
|
@ -562,7 +569,7 @@ function CreateKeyPageContent() {
|
|||
|
||||
{/* Claude Code Components */}
|
||||
<ClaudeCodePrompt
|
||||
isVisible={showClaudeCodePrompt}
|
||||
isVisible={showClaudeCodePrompt && !nudgesDisabled}
|
||||
onOpen={handleOpenClaudeCode}
|
||||
onDismiss={handleDismissClaudeCodePrompt}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export default function UISettings() {
|
|||
const allowVectorStoresTeamAdminsProperty = schema?.properties?.allow_vector_stores_for_team_admins;
|
||||
const scopeUserSearchProperty = schema?.properties?.scope_user_search_to_org;
|
||||
const disableCustomApiKeysProperty = schema?.properties?.disable_custom_api_keys;
|
||||
const disableUINudgesProperty = schema?.properties?.disable_ui_nudges;
|
||||
const values = data?.values ?? {};
|
||||
const isDisabledForInternalUsers = Boolean(values.disable_model_add_for_internal_users);
|
||||
const isDisabledTeamAdminDeleteTeamUser = Boolean(values.disable_team_admin_delete_team_user);
|
||||
|
|
@ -60,6 +61,20 @@ export default function UISettings() {
|
|||
);
|
||||
};
|
||||
|
||||
const handleToggleDisableUINudges = (checked: boolean) => {
|
||||
updateSettings(
|
||||
{ disable_ui_nudges: checked },
|
||||
{
|
||||
onSuccess: () => {
|
||||
NotificationManager.success("UI settings updated successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
NotificationManager.fromBackend(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleUpdatePageVisibility = (settings: { enabled_ui_pages_internal_users: string[] | null }) => {
|
||||
updateSettings(settings, {
|
||||
onSuccess: () => {
|
||||
|
|
@ -451,6 +466,26 @@ export default function UISettings() {
|
|||
|
||||
<Divider />
|
||||
|
||||
{/* Disable in-product UI nudges */}
|
||||
<Space align="start" size="middle">
|
||||
<Switch
|
||||
checked={Boolean(values.disable_ui_nudges)}
|
||||
disabled={isUpdating}
|
||||
loading={isUpdating}
|
||||
onChange={handleToggleDisableUINudges}
|
||||
aria-label={disableUINudgesProperty?.description ?? "Disable UI nudges"}
|
||||
/>
|
||||
<Space direction="vertical" size={4}>
|
||||
<Typography.Text strong>Disable UI nudges</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{disableUINudgesProperty?.description ??
|
||||
"If true, suppresses in-product UI nudges (survey and Claude Code feedback popups) for all users."}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Page Visibility for Internal Users */}
|
||||
<PageVisibilitySettings
|
||||
enabledPagesInternalUsers={values.enabled_ui_pages_internal_users}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue