feat(user_info_view.tsx): allow giving users more personal models

This commit is contained in:
Krrish Dholakia 2025-04-23 18:34:44 -07:00
parent 6896811cba
commit 358be06190
2 changed files with 279 additions and 89 deletions

View 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>
);
}

View file

@ -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;
@ -47,14 +48,21 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
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 +71,7 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
}
};
fetchUserData();
fetchData();
}, [accessToken, userId, userRole]);
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) {
return (
<div className="p-4">
@ -232,93 +266,115 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
{/* 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">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 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>
<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>