mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 03:04:13 +00:00
feat(user_info_view.tsx): allow giving users more personal models
This commit is contained in:
parent
6896811cba
commit
358be06190
2 changed files with 279 additions and 89 deletions
134
ui/litellm-dashboard/src/components/user_edit_view.tsx
Normal file
134
ui/litellm-dashboard/src/components/user_edit_view.tsx
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Form, Input, InputNumber, Select } from "antd";
|
||||||
|
import { Button } from "@tremor/react";
|
||||||
|
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||||
|
import { all_admin_roles } from "../utils/roles";
|
||||||
|
interface UserEditViewProps {
|
||||||
|
userData: any;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmit: (values: any) => void;
|
||||||
|
teams: any[] | null;
|
||||||
|
accessToken: string | null;
|
||||||
|
userID: string | null;
|
||||||
|
userRole: string | null;
|
||||||
|
userModels: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserEditView({
|
||||||
|
userData,
|
||||||
|
onCancel,
|
||||||
|
onSubmit,
|
||||||
|
teams,
|
||||||
|
accessToken,
|
||||||
|
userID,
|
||||||
|
userRole,
|
||||||
|
userModels,
|
||||||
|
}: 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"
|
||||||
|
>
|
||||||
|
<Input disabled />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Email"
|
||||||
|
name="user_email"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Role"
|
||||||
|
name="user_role"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Models"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Input.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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -14,9 +14,10 @@ import {
|
||||||
Badge,
|
Badge,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline";
|
import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline";
|
||||||
import { userInfoCall, userDeleteCall } from "../networking";
|
import { userInfoCall, userDeleteCall, userUpdateUserCall, modelAvailableCall } from "../networking";
|
||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
import { rolesWithWriteAccess } from '../../utils/roles';
|
import { rolesWithWriteAccess } from '../../utils/roles';
|
||||||
|
import { UserEditView } from "../user_edit_view";
|
||||||
|
|
||||||
interface UserInfoViewProps {
|
interface UserInfoViewProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
@ -47,14 +48,21 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
||||||
const [userData, setUserData] = useState<UserInfo | null>(null);
|
const [userData, setUserData] = useState<UserInfo | null>(null);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [userModels, setUserModels] = useState<string[]>([]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log(`userId: ${userId}, userRole: ${userRole}, accessToken: ${accessToken}`)
|
console.log(`userId: ${userId}, userRole: ${userRole}, accessToken: ${accessToken}`)
|
||||||
const fetchUserData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
const data = await userInfoCall(accessToken, userId, userRole || "", false, null, null, true);
|
const data = await userInfoCall(accessToken, userId, userRole || "", false, null, null, true);
|
||||||
setUserData(data);
|
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) {
|
} catch (error) {
|
||||||
console.error("Error fetching user data:", error);
|
console.error("Error fetching user data:", error);
|
||||||
message.error("Failed to fetch user data");
|
message.error("Failed to fetch user data");
|
||||||
|
@ -63,7 +71,7 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchUserData();
|
fetchData();
|
||||||
}, [accessToken, userId, userRole]);
|
}, [accessToken, userId, userRole]);
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
|
@ -81,6 +89,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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
@ -232,6 +266,27 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
||||||
{/* Details Panel */}
|
{/* Details Panel */}
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<Card>
|
<Card>
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Text className="font-medium">User ID</Text>
|
<Text className="font-medium">User ID</Text>
|
||||||
|
@ -319,6 +374,7 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue