mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-24 10:14:26 +00:00
Litellm multi admin fixes (#10259)
* fix(create_user_button.tsx): do not set 'no-default-models' when user is a proxy admin * fix(user_info_view.tsx): show all user personal models * feat(user_info_view.tsx): allow giving users more personal models * feat(user_edit_view.tsx): allow proxy admin to edit user role, available models, etc.
This commit is contained in:
parent
430cc60e62
commit
ab68af4ff5
5 changed files with 336 additions and 74 deletions
|
@ -117,13 +117,14 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleCreate = async (formValues: { user_id: string, models?: string[] }) => {
|
||||
const handleCreate = async (formValues: { user_id: string, models?: string[], user_role: string }) => {
|
||||
try {
|
||||
message.info("Making API Call");
|
||||
if (!isEmbedded) {
|
||||
setIsModalVisible(true);
|
||||
}
|
||||
if (!formValues.models || formValues.models.length === 0) {
|
||||
if ((!formValues.models || formValues.models.length === 0) && formValues.user_role !== "proxy_admin") {
|
||||
console.log("formValues.user_role", formValues.user_role)
|
||||
// If models is empty or undefined, set it to "no-default-models"
|
||||
formValues.models = ["no-default-models"];
|
||||
}
|
||||
|
|
168
ui/litellm-dashboard/src/components/user_edit_view.tsx
Normal file
168
ui/litellm-dashboard/src/components/user_edit_view.tsx
Normal file
|
@ -0,0 +1,168 @@
|
|||
import React from "react";
|
||||
import { Form, InputNumber, Select, Tooltip } from "antd";
|
||||
import { TextInput, Textarea, SelectItem } from "@tremor/react";
|
||||
import { Button } from "@tremor/react";
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import { all_admin_roles } from "../utils/roles";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
interface UserEditViewProps {
|
||||
userData: any;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: any) => void;
|
||||
teams: any[] | null;
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
userModels: string[];
|
||||
possibleUIRoles: Record<string, Record<string, string>> | null;
|
||||
}
|
||||
|
||||
export function UserEditView({
|
||||
userData,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
teams,
|
||||
accessToken,
|
||||
userID,
|
||||
userRole,
|
||||
userModels,
|
||||
possibleUIRoles,
|
||||
}: UserEditViewProps) {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// Set initial form values
|
||||
React.useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
user_id: userData.user_id,
|
||||
user_email: userData.user_info?.user_email,
|
||||
user_role: userData.user_info?.user_role,
|
||||
models: userData.user_info?.models || [],
|
||||
max_budget: userData.user_info?.max_budget,
|
||||
metadata: userData.user_info?.metadata ? JSON.stringify(userData.user_info.metadata, null, 2) : undefined,
|
||||
});
|
||||
}, [userData, form]);
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
// Convert metadata back to an object if it exists and is a string
|
||||
if (values.metadata && typeof values.metadata === "string") {
|
||||
try {
|
||||
values.metadata = JSON.parse(values.metadata);
|
||||
} catch (error) {
|
||||
console.error("Error parsing metadata JSON:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleSubmit}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
label="User ID"
|
||||
name="user_id"
|
||||
>
|
||||
<TextInput disabled />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Email"
|
||||
name="user_email"
|
||||
>
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={
|
||||
<span>
|
||||
Global Proxy Role{' '}
|
||||
<Tooltip title="This is the role that the user will globally on the proxy. This role is independent of any team/org specific roles.">
|
||||
<InfoCircleOutlined/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="user_role">
|
||||
<Select>
|
||||
{possibleUIRoles &&
|
||||
Object.entries(possibleUIRoles).map(
|
||||
([role, { ui_label, description }]) => (
|
||||
<SelectItem key={role} value={role} title={ui_label}>
|
||||
<div className="flex">
|
||||
{ui_label}{" "}
|
||||
<p
|
||||
className="ml-2"
|
||||
style={{ color: "gray", fontSize: "12px" }}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Personal Models{' '}
|
||||
<Tooltip title="Select which models this user can access outside of team-scope. Choose 'All Proxy Models' to grant access to all models available on the proxy.">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="models"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Select models"
|
||||
style={{ width: "100%" }}
|
||||
disabled={!all_admin_roles.includes(userData.user_info?.user_role || "")}
|
||||
|
||||
>
|
||||
<Select.Option key="all-proxy-models" value="all-proxy-models">
|
||||
All Proxy Models
|
||||
</Select.Option>
|
||||
{userModels.map((model) => (
|
||||
<Select.Option key={model} value={model}>
|
||||
{getModelDisplayName(model)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Max Budget (USD)"
|
||||
name="max_budget"
|
||||
>
|
||||
<InputNumber
|
||||
step={0.01}
|
||||
precision={2}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Metadata"
|
||||
name="metadata"
|
||||
>
|
||||
<Textarea
|
||||
rows={4}
|
||||
placeholder="Enter metadata as JSON"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
|
@ -651,6 +651,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
sortBy: filters.sort_by,
|
||||
sortOrder: filters.sort_order
|
||||
}}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
|
|
@ -31,6 +31,7 @@ interface UserDataTableProps {
|
|||
};
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
possibleUIRoles: Record<string, Record<string, string>> | null;
|
||||
}
|
||||
|
||||
export function UserDataTable({
|
||||
|
@ -41,6 +42,7 @@ export function UserDataTable({
|
|||
currentSort,
|
||||
accessToken,
|
||||
userRole,
|
||||
possibleUIRoles,
|
||||
}: UserDataTableProps) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([
|
||||
{
|
||||
|
@ -95,6 +97,7 @@ export function UserDataTable({
|
|||
onClose={handleCloseUserInfo}
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,9 +14,10 @@ import {
|
|||
Badge,
|
||||
} from "@tremor/react";
|
||||
import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import { userInfoCall, userDeleteCall } from "../networking";
|
||||
import { userInfoCall, userDeleteCall, userUpdateUserCall, modelAvailableCall } from "../networking";
|
||||
import { message } from "antd";
|
||||
import { rolesWithWriteAccess } from '../../utils/roles';
|
||||
import { UserEditView } from "../user_edit_view";
|
||||
|
||||
interface UserInfoViewProps {
|
||||
userId: string;
|
||||
|
@ -24,6 +25,7 @@ interface UserInfoViewProps {
|
|||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
onDelete?: () => void;
|
||||
possibleUIRoles: Record<string, Record<string, string>> | null;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
|
@ -43,18 +45,25 @@ interface UserInfo {
|
|||
teams: any[] | null;
|
||||
}
|
||||
|
||||
export default function UserInfoView({ userId, onClose, accessToken, userRole, onDelete }: UserInfoViewProps) {
|
||||
export default function UserInfoView({ userId, onClose, accessToken, userRole, onDelete, possibleUIRoles }: UserInfoViewProps) {
|
||||
const [userData, setUserData] = useState<UserInfo | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(`userId: ${userId}, userRole: ${userRole}, accessToken: ${accessToken}`)
|
||||
const fetchUserData = async () => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
const data = await userInfoCall(accessToken, userId, userRole || "", false, null, null, true);
|
||||
setUserData(data);
|
||||
|
||||
// Fetch available models
|
||||
const modelDataResponse = await modelAvailableCall(accessToken, userId, userRole || "");
|
||||
const availableModels = modelDataResponse.data.map((model: any) => model.id);
|
||||
setUserModels(availableModels);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user data:", error);
|
||||
message.error("Failed to fetch user data");
|
||||
|
@ -63,7 +72,7 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
|||
}
|
||||
};
|
||||
|
||||
fetchUserData();
|
||||
fetchData();
|
||||
}, [accessToken, userId, userRole]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
|
@ -81,6 +90,32 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
|||
}
|
||||
};
|
||||
|
||||
const handleUserUpdate = async (formValues: Record<string, any>) => {
|
||||
try {
|
||||
if (!accessToken || !userData) return;
|
||||
|
||||
const response = await userUpdateUserCall(accessToken, formValues, null);
|
||||
|
||||
// Update local state with new values
|
||||
setUserData({
|
||||
...userData,
|
||||
user_info: {
|
||||
...userData.user_info,
|
||||
user_email: formValues.user_email,
|
||||
models: formValues.models,
|
||||
max_budget: formValues.max_budget,
|
||||
metadata: formValues.metadata,
|
||||
}
|
||||
});
|
||||
|
||||
message.success("User updated successfully");
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error("Error updating user:", error);
|
||||
message.error("Failed to update user");
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
|
@ -213,81 +248,135 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
|||
<Text>{userData.keys?.length || 0} keys</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Personal Models</Text>
|
||||
<div className="mt-2">
|
||||
{userData.user_info?.models?.length && userData.user_info?.models?.length > 0 ? (
|
||||
userData.user_info?.models?.map((model, index) => (
|
||||
<Text key={index}>{model}</Text>
|
||||
))
|
||||
) : (
|
||||
<Text>All proxy models</Text>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
|
||||
{/* Details Panel */}
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">User ID</Text>
|
||||
<Text className="font-mono">{userData.user_id}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Email</Text>
|
||||
<Text>{userData.user_info?.user_email || "Not Set"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Role</Text>
|
||||
<Text>{userData.user_info?.user_role || "Not Set"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Created</Text>
|
||||
<Text>{userData.user_info?.created_at ? new Date(userData.user_info.created_at).toLocaleString() : "Unknown"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Last Updated</Text>
|
||||
<Text>{userData.user_info?.updated_at ? new Date(userData.user_info.updated_at).toLocaleString() : "Unknown"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Teams</Text>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{userData.teams?.length && userData.teams?.length > 0 ? (
|
||||
userData.teams?.map((team, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-blue-100 rounded text-xs"
|
||||
>
|
||||
{team.team_alias || team.team_id}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<Text>No teams</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">API Keys</Text>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{userData.keys?.length && userData.keys?.length > 0 ? (
|
||||
userData.keys.map((key, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-green-100 rounded text-xs"
|
||||
>
|
||||
{key.key_alias || key.token}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<Text>No API keys</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Metadata</Text>
|
||||
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
|
||||
{JSON.stringify(userData.user_info?.metadata || {}, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>User Settings</Title>
|
||||
{!isEditing && userRole && rolesWithWriteAccess.includes(userRole) && (
|
||||
<Button variant="light" onClick={() => setIsEditing(true)}>
|
||||
Edit Settings
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing && userData ? (
|
||||
<UserEditView
|
||||
userData={userData}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
onSubmit={handleUserUpdate}
|
||||
teams={userData.teams}
|
||||
accessToken={accessToken}
|
||||
userID={userId}
|
||||
userRole={userRole}
|
||||
userModels={userModels}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">User ID</Text>
|
||||
<Text className="font-mono">{userData.user_id}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Email</Text>
|
||||
<Text>{userData.user_info?.user_email || "Not Set"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Role</Text>
|
||||
<Text>{userData.user_info?.user_role || "Not Set"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Created</Text>
|
||||
<Text>{userData.user_info?.created_at ? new Date(userData.user_info.created_at).toLocaleString() : "Unknown"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Last Updated</Text>
|
||||
<Text>{userData.user_info?.updated_at ? new Date(userData.user_info.updated_at).toLocaleString() : "Unknown"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Teams</Text>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{userData.teams?.length && userData.teams?.length > 0 ? (
|
||||
userData.teams?.map((team, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-blue-100 rounded text-xs"
|
||||
>
|
||||
{team.team_alias || team.team_id}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<Text>No teams</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Models</Text>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{userData.user_info?.models?.length && userData.user_info?.models?.length > 0 ? (
|
||||
userData.user_info?.models?.map((model, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-blue-100 rounded text-xs"
|
||||
>
|
||||
{model}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<Text>All proxy models</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">API Keys</Text>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{userData.keys?.length && userData.keys?.length > 0 ? (
|
||||
userData.keys.map((key, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-green-100 rounded text-xs"
|
||||
>
|
||||
{key.key_alias || key.token}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<Text>No API keys</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Metadata</Text>
|
||||
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
|
||||
{JSON.stringify(userData.user_info?.metadata || {}, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue