mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 03:04:13 +00:00
ui expose default team settings on UI
This commit is contained in:
parent
890c49f150
commit
fb7ad6db44
3 changed files with 383 additions and 0 deletions
307
ui/litellm-dashboard/src/components/TeamSSOSettings.tsx
Normal file
307
ui/litellm-dashboard/src/components/TeamSSOSettings.tsx
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Card, Title, Text, Divider, Button, TextInput } from "@tremor/react";
|
||||||
|
import { Typography, Spin, message, Switch, Select, Form } from "antd";
|
||||||
|
import { getDefaultTeamSettings, updateDefaultTeamSettings, modelAvailableCall } from "./networking";
|
||||||
|
import BudgetDurationDropdown, { getBudgetDurationLabel } from "./common_components/budget_duration_dropdown";
|
||||||
|
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||||
|
|
||||||
|
interface TeamSSOSettingsProps {
|
||||||
|
accessToken: string | null;
|
||||||
|
userID: string;
|
||||||
|
userRole: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID, userRole }) => {
|
||||||
|
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 [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||||
|
const { Paragraph } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTeamSSOSettings = async () => {
|
||||||
|
if (!accessToken) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getDefaultTeamSettings(accessToken);
|
||||||
|
setSettings(data);
|
||||||
|
setEditedValues(data.values || {});
|
||||||
|
|
||||||
|
// Fetch available models
|
||||||
|
if (accessToken) {
|
||||||
|
try {
|
||||||
|
const modelResponse = await modelAvailableCall(accessToken, userID, userRole);
|
||||||
|
if (modelResponse && modelResponse.data) {
|
||||||
|
const modelNames = modelResponse.data.map((model: { id: string }) => model.id);
|
||||||
|
setAvailableModels(modelNames);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching available models:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching team SSO settings:", error);
|
||||||
|
message.error("Failed to fetch team settings");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTeamSSOSettings();
|
||||||
|
}, [accessToken]);
|
||||||
|
|
||||||
|
const handleSaveSettings = async () => {
|
||||||
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const updatedSettings = await updateDefaultTeamSettings(accessToken, editedValues);
|
||||||
|
setSettings({...settings, values: updatedSettings.settings});
|
||||||
|
setIsEditing(false);
|
||||||
|
message.success("Default team settings updated successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating team settings:", error);
|
||||||
|
message.error("Failed to update team settings");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTextInputChange = (key: string, value: any) => {
|
||||||
|
setEditedValues((prev: Record<string, any>) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderEditableField = (key: string, property: any, value: any) => {
|
||||||
|
const type = property.type;
|
||||||
|
|
||||||
|
if (key === "budget_duration") {
|
||||||
|
return (
|
||||||
|
<BudgetDurationDropdown
|
||||||
|
value={editedValues[key] || null}
|
||||||
|
onChange={(value) => handleTextInputChange(key, value)}
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else 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 (key === "models") {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={editedValues[key] || []}
|
||||||
|
onChange={(value) => handleTextInputChange(key, value)}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
{availableModels.map((model: string) => (
|
||||||
|
<Option key={model} value={model}>
|
||||||
|
{getModelDisplayName(model)}
|
||||||
|
</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 = (key: string, value: any): JSX.Element => {
|
||||||
|
if (value === null || value === undefined) return <span className="text-gray-400">Not set</span>;
|
||||||
|
|
||||||
|
if (key === "budget_duration") {
|
||||||
|
return <span>{getBudgetDurationLabel(value)}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return <span>{value ? "Enabled" : "Disabled"}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "models" && 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((model, index) => (
|
||||||
|
<span key={index} className="px-2 py-1 bg-blue-100 rounded text-xs">
|
||||||
|
{getModelDisplayName(model)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
|
||||||
|
{JSON.stringify(value, null, 2)}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{String(value)}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Text>No team settings available or you do not have permission to view them.</Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically render settings based on the schema
|
||||||
|
const renderSettings = () => {
|
||||||
|
const { values, schema } = settings;
|
||||||
|
|
||||||
|
if (!schema || !schema.properties) {
|
||||||
|
return <Text>No schema information available</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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>
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="mt-2">
|
||||||
|
{renderEditableField(key, property, value)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-1 p-2 bg-gray-50 rounded">
|
||||||
|
{renderValue(key, value)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<Title className="text-xl">Default Team Settings</Title>
|
||||||
|
{!loading && settings && (
|
||||||
|
isEditing ? (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditedValues(settings.values || {});
|
||||||
|
}}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveSettings}
|
||||||
|
loading={saving}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
>
|
||||||
|
Edit Settings
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
These settings will be applied by default when creating new teams.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{settings?.schema?.description && (
|
||||||
|
<Paragraph className="mb-4 mt-2">{settings.schema.description}</Paragraph>
|
||||||
|
)}
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
{renderSettings()}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamSSOSettings;
|
|
@ -4345,3 +4345,70 @@ export const tagDeleteCall = async (
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDefaultTeamSettings = async (accessToken: string) => {
|
||||||
|
try {
|
||||||
|
// Construct base URL
|
||||||
|
let url = proxyBaseUrl
|
||||||
|
? `${proxyBaseUrl}/get/default_team_settings`
|
||||||
|
: `/get/default_team_settings`;
|
||||||
|
|
||||||
|
console.log("Fetching default team settings from:", url);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
handleError(errorData);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Fetched default team settings:", data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch default team settings:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const updateDefaultTeamSettings = async (accessToken: string, settings: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
// Construct base URL
|
||||||
|
let url = proxyBaseUrl
|
||||||
|
? `${proxyBaseUrl}/update/default_team_settings`
|
||||||
|
: `/update/default_team_settings`;
|
||||||
|
|
||||||
|
console.log("Updating default team settings:", settings);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(settings),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
handleError(errorData);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Updated default team settings:", data);
|
||||||
|
message.success("Default team settings updated successfully");
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update default team settings:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
|
@ -27,6 +27,7 @@ import { Select, SelectItem } from "@tremor/react";
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { getGuardrailsList } from "./networking";
|
import { getGuardrailsList } from "./networking";
|
||||||
import TeamInfoView from "@/components/team/team_info";
|
import TeamInfoView from "@/components/team/team_info";
|
||||||
|
import TeamSSOSettings from "@/components/TeamSSOSettings";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
@ -354,6 +355,7 @@ const Teams: React.FC<TeamProps> = ({
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Tab>Your Teams</Tab>
|
<Tab>Your Teams</Tab>
|
||||||
<Tab>Available Teams</Tab>
|
<Tab>Available Teams</Tab>
|
||||||
|
<Tab>Default Team Settings</Tab>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
||||||
|
@ -797,6 +799,13 @@ const Teams: React.FC<TeamProps> = ({
|
||||||
userID={userID}
|
userID={userID}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<TeamSSOSettings
|
||||||
|
accessToken={accessToken}
|
||||||
|
userID={userID || ""}
|
||||||
|
userRole={userRole || ""}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
|
|
||||||
</TabGroup>)}
|
</TabGroup>)}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue