fix display of settings

This commit is contained in:
Ishaan Jaff 2025-03-17 16:56:49 -07:00
parent f69f330400
commit 0c0c7bf2f5
4 changed files with 166 additions and 76 deletions

View file

@ -2653,32 +2653,6 @@ class TeamKeyGenerationSettings(LiteLLMPydanticObjectBase):
) )
class PersonalKeyGenerationSettings(LiteLLMPydanticObjectBase):
"""
Settings for personal key generation (maps to 'Default Team' on UI)
"""
allowed_user_roles: Optional[List[str]] = Field(
default=[LitellmUserRoles.PROXY_ADMIN],
description="User roles that are allowed to generate personal keys (for the 'Default Team')",
)
class KeyGenerationSettings(LiteLLMPydanticObjectBase):
"""
Settings that restrict who can generate keys
"""
team_key_generation: Optional[TeamKeyGenerationSettings] = Field(
default=None,
description="Settings that control which team members can generate keys for their team",
)
personal_key_generation: Optional[PersonalKeyGenerationSettings] = Field(
default=None,
description="Settings that control which users can generate personal keys",
)
class DefaultInternalUserParams(LiteLLMPydanticObjectBase): class DefaultInternalUserParams(LiteLLMPydanticObjectBase):
""" """
Default parameters to apply when a new user signs in via SSO Default parameters to apply when a new user signs in via SSO
@ -2706,14 +2680,3 @@ class DefaultInternalUserParams(LiteLLMPydanticObjectBase):
models: Optional[List[str]] = Field( models: Optional[List[str]] = Field(
default=None, description="Default list of models that new users can access" default=None, description="Default list of models that new users can access"
) )
class UISSOSettings(LiteLLMPydanticObjectBase):
"""
Configuration for SSO integration with the LiteLLM proxy UI
"""
default_internal_user_params: Optional[DefaultInternalUserParams] = Field(
default=None,
description="Default parameters applied to new users signing in via SSO",
)

View file

@ -112,7 +112,7 @@ async def delete_allowed_ip(ip_address: IPAddress):
@router.get( @router.get(
"/sso_settings", "/get/internal_user_settings",
tags=["SSO Settings"], tags=["SSO Settings"],
dependencies=[Depends(user_api_key_auth)], dependencies=[Depends(user_api_key_auth)],
) )
@ -124,27 +124,15 @@ async def get_sso_settings():
from pydantic import TypeAdapter from pydantic import TypeAdapter
# Create the settings object first # Create the settings object first
sso_settings = UISSOSettings( sso_settings = DefaultInternalUserParams(
max_internal_user_budget=litellm.max_internal_user_budget, **(
internal_user_budget_duration=litellm.internal_user_budget_duration, litellm.default_internal_user_params
default_internal_user_params=DefaultInternalUserParams( if isinstance(litellm.default_internal_user_params, dict)
**( else {}
litellm.default_internal_user_params )
if isinstance(litellm.default_internal_user_params, dict)
else {}
)
),
upperbound_key_generate_params=UpperboundKeyGenerateParams(
**(
litellm.upperbound_key_generate_params
if isinstance(litellm.upperbound_key_generate_params, dict)
else {}
)
),
) )
# Get the schema for UISSOSettings # Get the schema for UISSOSettings
schema = TypeAdapter(UISSOSettings).json_schema(by_alias=True) schema = TypeAdapter(DefaultInternalUserParams).json_schema(by_alias=True)
# Convert to dict for response # Convert to dict for response
settings_dict = sso_settings.model_dump() settings_dict = sso_settings.model_dump()

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Card, Title, Text, Divider } from "@tremor/react"; import { Card, Title, Text, Divider, Button, TextInput } from "@tremor/react";
import { Typography, Spin, message } from "antd"; import { Typography, Spin, message, Switch, Select, Form } from "antd";
import { getSSOSettingsCall } from "./networking"; import { getInternalUserSettings, updateInternalUserSettings } from "./networking";
interface SSOSettingsProps { interface SSOSettingsProps {
accessToken: string | null; accessToken: string | null;
@ -10,7 +10,11 @@ interface SSOSettingsProps {
const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => { const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [settings, setSettings] = useState<any>(null); const [settings, setSettings] = useState<any>(null);
const [isEditing, setIsEditing] = useState<boolean>(false);
const [editedValues, setEditedValues] = useState<any>({});
const [saving, setSaving] = useState<boolean>(false);
const { Paragraph } = Typography; const { Paragraph } = Typography;
const { Option } = Select;
useEffect(() => { useEffect(() => {
const fetchSSOSettings = async () => { const fetchSSOSettings = async () => {
@ -20,8 +24,9 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
} }
try { try {
const data = await getSSOSettingsCall(accessToken); const data = await getInternalUserSettings(accessToken);
setSettings(data); setSettings(data);
setEditedValues(data.values || {});
} catch (error) { } catch (error) {
console.error("Error fetching SSO settings:", error); console.error("Error fetching SSO settings:", error);
message.error("Failed to fetch SSO settings"); message.error("Failed to fetch SSO settings");
@ -33,12 +38,105 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
fetchSSOSettings(); fetchSSOSettings();
}, [accessToken]); }, [accessToken]);
const handleSaveSettings = async () => {
if (!accessToken) return;
setSaving(true);
try {
await updateInternalUserSettings(accessToken, editedValues);
setSettings({...settings, values: editedValues});
setIsEditing(false);
message.success("Settings updated successfully");
} catch (error) {
console.error("Error updating SSO settings:", error);
message.error("Failed to update settings");
} finally {
setSaving(false);
}
};
const handleTextInputChange = (key: string, value: any) => {
setEditedValues(prev => ({
...prev,
[key]: value
}));
};
const renderEditableField = (key: string, property: any, value: any) => {
const type = property.type;
if (type === "boolean") {
return (
<div className="mt-2">
<Switch
checked={!!editedValues[key]}
onChange={(checked) => handleTextInputChange(key, checked)}
/>
</div>
);
} else if (type === "array" && property.items?.enum) {
return (
<Select
mode="multiple"
style={{ width: '100%' }}
value={editedValues[key] || []}
onChange={(value) => handleTextInputChange(key, value)}
className="mt-2"
>
{property.items.enum.map((option: string) => (
<Option key={option} value={option}>{option}</Option>
))}
</Select>
);
} else if (type === "string" && property.enum) {
return (
<Select
style={{ width: '100%' }}
value={editedValues[key] || ""}
onChange={(value) => handleTextInputChange(key, value)}
className="mt-2"
>
{property.enum.map((option: string) => (
<Option key={option} value={option}>{option}</Option>
))}
</Select>
);
} else {
return (
<TextInput
value={editedValues[key] !== undefined ? String(editedValues[key]) : ""}
onChange={(e) => handleTextInputChange(key, e.target.value)}
placeholder={property.description || ""}
className="mt-2"
/>
);
}
};
const renderValue = (value: any): JSX.Element => { const renderValue = (value: any): JSX.Element => {
if (value === null) return <span className="text-gray-400">Not set</span>; if (value === null || value === undefined) return <span className="text-gray-400">Not set</span>;
if (typeof value === "boolean") {
return <span>{value ? "Enabled" : "Disabled"}</span>;
}
if (typeof value === "object") { if (typeof value === "object") {
if (Array.isArray(value)) {
if (value.length === 0) return <span className="text-gray-400">None</span>;
return (
<div className="flex flex-wrap gap-2 mt-1">
{value.map((item, index) => (
<span key={index} className="px-2 py-1 bg-blue-100 rounded text-xs">
{typeof item === "object" ? JSON.stringify(item) : String(item)}
</span>
))}
</div>
);
}
return ( return (
<pre className="bg-gray-50 p-2 rounded overflow-auto max-h-60"> <pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
{JSON.stringify(value, null, 2)} {JSON.stringify(value, null, 2)}
</pre> </pre>
); );
@ -58,7 +156,7 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
if (!settings) { if (!settings) {
return ( return (
<Card> <Card>
<Title>Personal Key Creation</Title> <Title>SSO Settings</Title>
<Text>No settings available or you don't have permission to view them.</Text> <Text>No settings available or you don't have permission to view them.</Text>
</Card> </Card>
); );
@ -74,16 +172,24 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
return Object.entries(schema.properties).map(([key, property]: [string, any]) => { return Object.entries(schema.properties).map(([key, property]: [string, any]) => {
const value = values[key]; const value = values[key];
const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
return ( return (
<div key={key} className="mb-6"> <div key={key} className="mb-6 pb-6 border-b border-gray-200 last:border-0">
<Text className="font-medium text-lg">{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</Text> <Text className="font-medium text-lg">{displayName}</Text>
<Paragraph className="text-sm text-gray-500 mt-1"> <Paragraph className="text-sm text-gray-500 mt-1">
{property.description || "No description available"} {property.description || "No description available"}
</Paragraph> </Paragraph>
<div className="mt-2 p-3 bg-gray-50 rounded-md">
{renderValue(value)} {isEditing ? (
</div> <div className="mt-2">
{renderEditableField(key, property, value)}
</div>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{renderValue(value)}
</div>
)}
</div> </div>
); );
}); });
@ -91,13 +197,46 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
return ( return (
<Card> <Card>
<Title>SSO Settings</Title> <div className="flex justify-between items-center mb-4">
{settings.schema?.description && ( <Title>SSO Settings</Title>
<Paragraph>{settings.schema.description}</Paragraph> {!loading && settings && (
isEditing ? (
<div className="flex gap-2">
<Button
color="gray"
onClick={() => {
setIsEditing(false);
setEditedValues(settings.values || {});
}}
disabled={saving}
>
Cancel
</Button>
<Button
color="blue"
onClick={handleSaveSettings}
loading={saving}
>
Save Changes
</Button>
</div>
) : (
<Button
variant="light"
onClick={() => setIsEditing(true)}
>
Edit Settings
</Button>
)
)}
</div>
{settings?.schema?.description && (
<Paragraph className="mb-4">{settings.schema.description}</Paragraph>
)} )}
<Divider /> <Divider />
<div className="mt-4"> <div className="mt-4 space-y-4">
{renderSettings()} {renderSettings()}
</div> </div>
</Card> </Card>

View file

@ -3974,12 +3974,12 @@ export const uiSpendLogDetailsCall = async (
} }
}; };
export const getSSOSettingsCall = async (accessToken: string) => { export const getInternalUserSettings = async (accessToken: string) => {
try { try {
// Construct base URL // Construct base URL
let url = proxyBaseUrl let url = proxyBaseUrl
? `${proxyBaseUrl}/sso_settings` ? `${proxyBaseUrl}/get/internal_user_settings`
: `/sso_settings`; : `/get/internal_user_settings`;
console.log("Fetching SSO settings from:", url); console.log("Fetching SSO settings from:", url);