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):
"""
Default parameters to apply when a new user signs in via SSO
@ -2706,14 +2680,3 @@ class DefaultInternalUserParams(LiteLLMPydanticObjectBase):
models: Optional[List[str]] = Field(
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(
"/sso_settings",
"/get/internal_user_settings",
tags=["SSO Settings"],
dependencies=[Depends(user_api_key_auth)],
)
@ -124,27 +124,15 @@ async def get_sso_settings():
from pydantic import TypeAdapter
# Create the settings object first
sso_settings = UISSOSettings(
max_internal_user_budget=litellm.max_internal_user_budget,
internal_user_budget_duration=litellm.internal_user_budget_duration,
default_internal_user_params=DefaultInternalUserParams(
sso_settings = DefaultInternalUserParams(
**(
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
schema = TypeAdapter(UISSOSettings).json_schema(by_alias=True)
schema = TypeAdapter(DefaultInternalUserParams).json_schema(by_alias=True)
# Convert to dict for response
settings_dict = sso_settings.model_dump()

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { Card, Title, Text, Divider } from "@tremor/react";
import { Typography, Spin, message } from "antd";
import { getSSOSettingsCall } from "./networking";
import { Card, Title, Text, Divider, Button, TextInput } from "@tremor/react";
import { Typography, Spin, message, Switch, Select, Form } from "antd";
import { getInternalUserSettings, updateInternalUserSettings } from "./networking";
interface SSOSettingsProps {
accessToken: string | null;
@ -10,7 +10,11 @@ interface SSOSettingsProps {
const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
const [loading, setLoading] = useState<boolean>(true);
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 { Option } = Select;
useEffect(() => {
const fetchSSOSettings = async () => {
@ -20,8 +24,9 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
}
try {
const data = await getSSOSettingsCall(accessToken);
const data = await getInternalUserSettings(accessToken);
setSettings(data);
setEditedValues(data.values || {});
} catch (error) {
console.error("Error fetching SSO settings:", error);
message.error("Failed to fetch SSO settings");
@ -33,12 +38,105 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
fetchSSOSettings();
}, [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 => {
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 (Array.isArray(value)) {
if (value.length === 0) return <span className="text-gray-400">None</span>;
return (
<pre className="bg-gray-50 p-2 rounded overflow-auto max-h-60">
<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 (
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
{JSON.stringify(value, null, 2)}
</pre>
);
@ -58,7 +156,7 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
if (!settings) {
return (
<Card>
<Title>Personal Key Creation</Title>
<Title>SSO Settings</Title>
<Text>No settings available or you don't have permission to view them.</Text>
</Card>
);
@ -74,16 +172,24 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
return Object.entries(schema.properties).map(([key, property]: [string, any]) => {
const value = values[key];
const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
return (
<div key={key} className="mb-6">
<Text className="font-medium text-lg">{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</Text>
<div key={key} className="mb-6 pb-6 border-b border-gray-200 last:border-0">
<Text className="font-medium text-lg">{displayName}</Text>
<Paragraph className="text-sm text-gray-500 mt-1">
{property.description || "No description available"}
</Paragraph>
<div className="mt-2 p-3 bg-gray-50 rounded-md">
{isEditing ? (
<div className="mt-2">
{renderEditableField(key, property, value)}
</div>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{renderValue(value)}
</div>
)}
</div>
);
});
@ -91,13 +197,46 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
return (
<Card>
<div className="flex justify-between items-center mb-4">
<Title>SSO Settings</Title>
{settings.schema?.description && (
<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 />
<div className="mt-4">
<div className="mt-4 space-y-4">
{renderSettings()}
</div>
</Card>

View file

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