feat(ui): user_edit_view to shadcn

- @tremor Button/SelectItem/TextInput/Textarea + antd Checkbox/Tooltip
  + @ant-design InfoCircleOutlined \u2192 shadcn Button/Input/Textarea/
  Checkbox/Tooltip + lucide Info.
- antd Form/Form.Item/Select preserved (form.setFieldsValue +
  validation rules). Role + models multi-select kept on antd Select
  since shadcn Select doesn't support multiple.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 15:10:26 +00:00
parent 1f4656bc4c
commit afa60be4af
No known key found for this signature in database

View file

@ -1,6 +1,15 @@
import { InfoCircleOutlined } from "@ant-design/icons";
import { Button, SelectItem, TextInput, Textarea } from "@tremor/react";
import { Checkbox, Form, Select, Tooltip } from "antd";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Form, Select } from "antd";
import { Info } from "lucide-react";
import React, { useState } from "react";
import { all_admin_roles } from "../utils/roles";
import BudgetDurationDropdown from "./common_components/budget_duration_dropdown";
@ -8,9 +17,12 @@ import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_t
import NumericalInput from "./shared/numerical_input";
interface UserEditViewProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
userData: any;
onCancel: () => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSubmit: (values: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
teams: any[] | null;
accessToken: string | null;
userID: string | null;
@ -24,8 +36,11 @@ export function UserEditView({
userData,
onCancel,
onSubmit,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
teams,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
accessToken,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userID,
userRole,
userModels,
@ -35,7 +50,6 @@ export function UserEditView({
const [form] = Form.useForm();
const [unlimitedBudget, setUnlimitedBudget] = useState(false);
// Set initial form values
React.useEffect(() => {
const maxBudget = userData.user_info?.max_budget;
const isUnlimited = maxBudget === null || maxBudget === undefined;
@ -49,20 +63,21 @@ export function UserEditView({
models: userData.user_info?.models || [],
max_budget: isUnlimited ? "" : maxBudget,
budget_duration: userData.user_info?.budget_duration,
metadata: userData.user_info?.metadata ? JSON.stringify(userData.user_info.metadata, null, 2) : undefined,
metadata: userData.user_info?.metadata
? JSON.stringify(userData.user_info.metadata, null, 2)
: undefined,
});
}, [userData, form]);
const handleUnlimitedBudgetChange = (e: any) => {
const checked = e.target.checked;
const handleUnlimitedBudgetChange = (checked: boolean) => {
setUnlimitedBudget(checked);
if (checked) {
form.setFieldsValue({ max_budget: "" });
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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);
@ -72,7 +87,11 @@ export function UserEditView({
}
}
if (unlimitedBudget || values.max_budget === "" || values.max_budget === undefined) {
if (
unlimitedBudget ||
values.max_budget === "" ||
values.max_budget === undefined
) {
values.max_budget = null;
}
@ -83,43 +102,53 @@ export function UserEditView({
<Form form={form} onFinish={handleSubmit} layout="vertical">
{!isBulkEdit && (
<Form.Item label="User ID" name="user_id">
<TextInput disabled />
<Input disabled />
</Form.Item>
)}
{!isBulkEdit && (
<Form.Item label="Email" name="user_email">
<TextInput />
<Input />
</Form.Item>
)}
<Form.Item label="User Alias" name="user_alias">
<TextInput />
<Input />
</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>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-1 h-3 w-3 inline text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
This is the role that the user will globally on the proxy.
This role is independent of any team/org specific roles.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</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>
))}
Object.entries(possibleUIRoles).map(
([role, { ui_label, description }]) => (
<Select.Option key={role} value={role} title={ui_label}>
<div className="flex">
{ui_label}{" "}
<p className="ml-2 text-muted-foreground text-xs">
{description}
</p>
</div>
</Select.Option>
),
)}
</Select>
</Form.Item>
@ -127,9 +156,18 @@ export function UserEditView({
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>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-1 h-3 w-3 inline text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Select which models this user can access outside of
team-scope. Choose &apos;All Proxy Models&apos; to grant
access to all models available on the proxy.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
}
name="models"
@ -156,22 +194,30 @@ export function UserEditView({
<Form.Item
label={
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<div className="flex items-center gap-3">
<span>Max Budget (USD)</span>
<Checkbox
checked={unlimitedBudget}
onChange={handleUnlimitedBudgetChange}
>
Unlimited Budget
</Checkbox>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={unlimitedBudget}
onCheckedChange={(c) => handleUnlimitedBudgetChange(c === true)}
/>
<span className="text-sm">Unlimited Budget</span>
</label>
</div>
}
name="max_budget"
rules={[
{
validator: (_, value) => {
if (!unlimitedBudget && (value === "" || value === null || value === undefined)) {
return Promise.reject(new Error("Please enter a budget or select Unlimited Budget"));
if (
!unlimitedBudget &&
(value === "" || value === null || value === undefined)
) {
return Promise.reject(
new Error(
"Please enter a budget or select Unlimited Budget",
),
);
}
return Promise.resolve();
},