diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json
index 73e88197b19..47a25bf9748 100644
--- a/ui/litellm-dashboard/.eslintrc.json
+++ b/ui/litellm-dashboard/.eslintrc.json
@@ -146,7 +146,11 @@
"src/components/price_data_reload.tsx",
"src/components/mcp_tools/mcp_connect.tsx",
"src/components/mcp_tools/mcp_tools.tsx",
- "src/components/mcp_tools/ToolTestPanel.tsx"
+ "src/components/mcp_tools/ToolTestPanel.tsx",
+ "src/components/bulk_create_users_button.tsx",
+ "src/components/BulkEditUsers.tsx",
+ "src/components/DefaultUserSettings.tsx",
+ "src/components/view_users/user_info_view.tsx"
],
"rules": {
"litellm-ui/no-banned-ui-imports": "off",
diff --git a/ui/litellm-dashboard/docs/DEVIATIONS.md b/ui/litellm-dashboard/docs/DEVIATIONS.md
index ce9d873f0e6..6ddbfddb571 100644
--- a/ui/litellm-dashboard/docs/DEVIATIONS.md
+++ b/ui/litellm-dashboard/docs/DEVIATIONS.md
@@ -60,6 +60,26 @@ Format per entry:
policy tokens (`--policy-untrusted`, `--policy-trusted`,
`--policy-blocked`) backed by CSS variables; out of scope for phase 1.
+## Users section — categorical colors (red/green/blue/yellow) in bulk-create + edit status UI
+
+- **Type:** `blueprint deviation`
+- **Blueprint rule deviated from:** §1 "semantic Tailwind tokens only".
+- **What the agent did instead:** `bulk_create_users_button.tsx`,
+ `BulkEditUsers.tsx`, `DefaultUserSettings.tsx`, and
+ `view_users/user_info_view.tsx` keep raw palette classes
+ (`bg-red-50`/`text-red-500`, `bg-green-100`/`text-green-500`,
+ `bg-blue-50`/`text-blue-500`, `bg-yellow-50`/`text-yellow-700`,
+ `bg-blue-100` model chips, `text-amber-600` warning callout, etc.) and
+ are added to the `litellm-ui/no-raw-tailwind-colors` override list in
+ `.eslintrc.json`.
+- **Why:** The CSV import flow uses red = invalid / failed, green =
+ success, blue = info / pending, yellow = structural warning — a
+ categorical, status-driven palette with four distinct hues. Only
+ `destructive` maps cleanly; `primary`/`muted` don't cover "success" or
+ "info/pending" as separate categories. A phase-2 task could introduce
+ semantic status tokens (`--status-success`, `--status-info`,
+ `--status-warning`, `--status-failure`); out of scope for phase 1.
+
## Section 1 (Access Groups) — custom MultiSelect shim in lieu of antd Select mode=multiple
- **Type:** `blueprint deviation`
diff --git a/ui/litellm-dashboard/src/components/BulkEditUsers.tsx b/ui/litellm-dashboard/src/components/BulkEditUsers.tsx
index 2ca69065bc1..c7db2842813 100644
--- a/ui/litellm-dashboard/src/components/BulkEditUsers.tsx
+++ b/ui/litellm-dashboard/src/components/BulkEditUsers.tsx
@@ -1,21 +1,42 @@
-import React, { useState } from "react";
+import React, { useMemo, useState } from "react";
+import { X } from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import { Card } from "@/components/ui/card";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Label } from "@/components/ui/label";
+import { Separator } from "@/components/ui/separator";
import {
- Modal,
- Typography,
- Divider,
- Table,
Select,
- InputNumber,
- Card,
- Space,
- Checkbox,
-} from "antd";
-import { userBulkUpdateUserCall, teamBulkMemberAddCall, Member } from "./networking";
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+import MessageManager from "@/components/molecules/message_manager";
+import {
+ userBulkUpdateUserCall,
+ teamBulkMemberAddCall,
+ Member,
+} from "./networking";
import { UserEditView } from "./user_edit_view";
import NotificationsManager from "./molecules/notifications_manager";
-import MessageManager from "@/components/molecules/message_manager";
-
-const { Text, Title } = Typography;
+import NumericalInput from "./shared/numerical_input";
interface BulkEditUserModalProps {
open: boolean;
@@ -30,6 +51,79 @@ interface BulkEditUserModalProps {
allowAllUsers?: boolean; // Optional flag to enable "all users" mode
}
+/**
+ * shadcn Select + Badge chip multi-select for teams.
+ */
+function TeamMultiSelect({
+ value,
+ onChange,
+ options,
+ placeholder,
+}: {
+ value: string[];
+ onChange: (next: string[]) => void;
+ options: { label: string; value: string }[];
+ placeholder: string;
+}) {
+ const selected = useMemo(() => value ?? [], [value]);
+ const remaining = useMemo(
+ () => options.filter((o) => !selected.includes(o.value)),
+ [options, selected],
+ );
+
+ return (
+
+
{
+ if (v) onChange([...selected, v]);
+ }}
+ >
+
+
+
+
+ {remaining.length === 0 ? (
+
+ No more teams available
+
+ ) : (
+ remaining.map((opt) => (
+
+ {opt.label}
+
+ ))
+ )}
+
+
+ {selected.length > 0 && (
+
+ {selected.map((v) => {
+ const opt = options.find((o) => o.value === v);
+ return (
+
+ {opt?.label ?? v}
+ onChange(selected.filter((s) => s !== v))}
+ className="inline-flex items-center justify-center rounded-full hover:bg-muted-foreground/20"
+ aria-label={`Remove ${opt?.label ?? v}`}
+ >
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
const BulkEditUserModal: React.FC = ({
open,
onCancel,
@@ -49,7 +143,6 @@ const BulkEditUserModal: React.FC = ({
const [updateAllUsers, setUpdateAllUsers] = useState(false);
const handleCancel = () => {
- // Reset team management state
setSelectedTeams([]);
setTeamBudget(null);
setAddToTeams(false);
@@ -75,6 +168,7 @@ const BulkEditUserModal: React.FC = ({
keys: [],
teams: teams || [],
}),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[teams, open],
);
@@ -89,7 +183,6 @@ const BulkEditUserModal: React.FC = ({
try {
const userIds = selectedUsers.map((user) => user.user_id);
- // Build the update payload - only include fields that have been changed from default/empty values
const updatePayload: any = {};
if (formValues.user_role && formValues.user_role !== "") {
@@ -108,46 +201,54 @@ const BulkEditUserModal: React.FC = ({
updatePayload.budget_duration = formValues.budget_duration;
}
- if (formValues.metadata && Object.keys(formValues.metadata).length > 0) {
+ if (
+ formValues.metadata &&
+ Object.keys(formValues.metadata).length > 0
+ ) {
updatePayload.metadata = formValues.metadata;
}
- // Check if any operations were requested
const hasUserUpdates = Object.keys(updatePayload).length > 0;
const hasTeamAdditions = addToTeams && selectedTeams.length > 0;
if (!hasUserUpdates && !hasTeamAdditions) {
- NotificationsManager.fromBackend("Please modify at least one field or select teams to add users to");
+ NotificationsManager.fromBackend(
+ "Please modify at least one field or select teams to add users to",
+ );
return;
}
let successMessages: string[] = [];
- // Handle user property updates
if (hasUserUpdates) {
if (updateAllUsers) {
- const result = await userBulkUpdateUserCall(accessToken, updatePayload, undefined, true);
- successMessages.push(`Updated all users (${result.total_requested} total)`);
+ const result = await userBulkUpdateUserCall(
+ accessToken,
+ updatePayload,
+ undefined,
+ true,
+ );
+ successMessages.push(
+ `Updated all users (${result.total_requested} total)`,
+ );
} else {
await userBulkUpdateUserCall(accessToken, updatePayload, userIds);
successMessages.push(`Updated ${userIds.length} user(s)`);
}
}
- // Handle team additions
if (hasTeamAdditions) {
const teamResults: any[] = [];
for (const teamId of selectedTeams) {
try {
- // Create member objects for bulk add
let members: Member[] | null = null;
if (updateAllUsers) {
members = null;
} else {
members = selectedUsers.map((user) => ({
user_id: user.user_id,
- role: "user" as const, // Default role for bulk add
+ role: "user" as const,
user_email: user.user_email || null,
}));
}
@@ -178,17 +279,23 @@ const BulkEditUserModal: React.FC = ({
}
}
- // Generate team success message
const successfulTeams = teamResults.filter((r) => r.success);
const failedTeams = teamResults.filter((r) => !r.success);
if (successfulTeams.length > 0) {
- const totalAdditions = successfulTeams.reduce((sum, r) => sum + r.successfulAdditions, 0);
- successMessages.push(`Added users to ${successfulTeams.length} team(s) (${totalAdditions} total additions)`);
+ const totalAdditions = successfulTeams.reduce(
+ (sum, r) => sum + r.successfulAdditions,
+ 0,
+ );
+ successMessages.push(
+ `Added users to ${successfulTeams.length} team(s) (${totalAdditions} total additions)`,
+ );
}
if (failedTeams.length > 0) {
- MessageManager.warning(`Failed to add users to ${failedTeams.length} team(s)`);
+ MessageManager.warning(
+ `Failed to add users to ${failedTeams.length} team(s)`,
+ );
}
}
@@ -196,7 +303,6 @@ const BulkEditUserModal: React.FC = ({
NotificationsManager.success(successMessages.join(". "));
}
- // Reset team management state
setSelectedTeams([]);
setTeamBudget(null);
setAddToTeams(false);
@@ -212,164 +318,176 @@ const BulkEditUserModal: React.FC = ({
}
};
+ const teamOptions = (teams || []).map((team: any) => ({
+ label: team.team_alias || team.team_id,
+ value: team.team_id,
+ }));
+
return (
- (!o ? handleCancel() : undefined)}
>
- {allowAllUsers && (
-
-
setUpdateAllUsers(e.target.checked)}>
- Update ALL users in the system
-
- {updateAllUsers && (
-
-
- ⚠️ This will apply changes to ALL users in the system, not just the selected ones.
-
+
+
+
+ {updateAllUsers
+ ? "Bulk Edit All Users"
+ : `Bulk Edit ${selectedUsers.length} User(s)`}
+
+
+
+ {allowAllUsers && (
+
+
+ setUpdateAllUsers(c === true)}
+ />
+
+ Update ALL users in the system
+
+
+ {updateAllUsers && (
+
+
+ ⚠️ This will apply changes to ALL users in the system, not
+ just the selected ones.
+
+
+ )}
+
+ )}
+
+ {!updateAllUsers && (
+
+
+ Selected Users ({selectedUsers.length}):
+
+
+
+
+
+ User ID
+ Email
+ Current Role
+ Budget
+
+
+
+ {selectedUsers.map((user) => (
+
+
+ {user.user_id.length > 20
+ ? `${user.user_id.slice(0, 20)}...`
+ : user.user_id}
+
+
+ {user.user_email || "No email"}
+
+
+ {possibleUIRoles?.[user.user_role]?.ui_label ||
+ user.user_role}
+
+
+ {user.max_budget !== null
+ ? `$${user.max_budget}`
+ : "Unlimited"}
+
+
+ ))}
+
+
- )}
-
- )}
+
+ )}
+
+
- {!updateAllUsers && (
-
Selected Users ({selectedUsers.length}):
-
(
-
- {text.length > 20 ? `${text.slice(0, 20)}...` : text}
-
- ),
- },
- {
- title: "Email",
- dataIndex: "user_email",
- key: "user_email",
- width: "25%",
- render: (text: string) => (
-
- {text || "No email"}
-
- ),
- },
- {
- title: "Current Role",
- dataIndex: "user_role",
- key: "user_role",
- width: "25%",
- render: (role: string) => (
- {possibleUIRoles?.[role]?.ui_label || role}
- ),
- },
- {
- title: "Budget",
- dataIndex: "max_budget",
- key: "max_budget",
- width: "20%",
- render: (budget: number | null) => (
- {budget !== null ? `$${budget}` : "Unlimited"}
- ),
- },
- ]}
- />
+
+ Instructions: Fill in the fields below with the
+ values you want to apply to all selected users. You can bulk edit:
+ role, budget, models, and metadata. You can also add users to
+ teams.
+
- )}
-
+ {/* Team Management Section */}
+
+ Team Management
+
+
+ setAddToTeams(c === true)}
+ />
+ Add selected users to teams
+
-
-
- Instructions: Fill in the fields below with the values you want to apply to all selected
- users. You can bulk edit: role, budget, models, and metadata. You can also add users to teams.
-
-
+ {addToTeams && (
+ <>
+
- {/* Team Management Section */}
-
-
- setAddToTeams(e.target.checked)}>
- Add selected users to teams
-
+
+
Team Budget (Optional):
+
+ setTeamBudget(v)}
+ min={0}
+ step={0.01}
+ precision={2}
+ />
+
+
+ Leave empty for unlimited budget within team limits
+
+
- {addToTeams && (
- <>
-
- Select Teams:
- ({
- label: team.team_alias || team.team_id,
- value: team.team_id,
- })) || []
- }
- />
-
+
+ Users will be added with "user" role by default.
+ All users will be added to each selected team.
+
+ >
+ )}
+
+
-
- Team Budget (Optional):
- setTeamBudget(value)}
- style={{ width: "100%", marginTop: 8 }}
- min={0}
- step={0.01}
- precision={2}
- />
-
- Leave empty for unlimited budget within team limits
-
-
+
-
- Users will be added with "user" role by default. All users will be added to each selected
- team.
-
- >
- )}
-
-
-
-
-
- {loading && (
-
- Updating {updateAllUsers ? "all users" : selectedUsers.length} user(s)...
-
- )}
-
+ {loading && (
+
+
+ Updating{" "}
+ {updateAllUsers ? "all users" : selectedUsers.length} user(s)...
+
+
+ )}
+
+
);
};
diff --git a/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx b/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx
index 03d982ca7c2..207f7be03f9 100644
--- a/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx
+++ b/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx
@@ -103,8 +103,8 @@ describe("CreateUserButton", () => {
,
);
await userEvent.click(screen.getByRole("combobox", { name: /user role/i }));
- expect(screen.getByText("Admin")).toBeInTheDocument();
- expect(screen.getByText("User")).toBeInTheDocument();
+ expect(screen.getByRole("option", { name: /Admin/i })).toBeInTheDocument();
+ expect(screen.getByRole("option", { name: /User/i })).toBeInTheDocument();
});
it("should close modal when cancel is clicked in standalone mode", async () => {
@@ -139,7 +139,7 @@ describe("CreateUserButton", () => {
await user.type(screen.getByLabelText(/user email/i), "test@example.com");
await user.click(screen.getByRole("combobox", { name: /user role/i }));
- await user.click(screen.getByText("User"));
+ await user.click(screen.getByRole("option", { name: /User/i }));
await user.click(screen.getByRole("button", { name: /create user/i }));
await waitFor(() => {
@@ -161,7 +161,7 @@ describe("CreateUserButton", () => {
await user.type(screen.getByLabelText(/user email/i), "embedded@example.com");
await user.click(screen.getByRole("combobox", { name: /user role/i }));
- await user.click(screen.getByText("User"));
+ await user.click(screen.getByRole("option", { name: /User/i }));
await user.click(screen.getByRole("button", { name: /create user/i }));
await waitFor(() => {
@@ -179,7 +179,7 @@ describe("CreateUserButton", () => {
await user.type(screen.getByLabelText(/user email/i), "duplicate@example.com");
await user.click(screen.getByRole("combobox", { name: /user role/i }));
- await user.click(screen.getByText("User"));
+ await user.click(screen.getByRole("option", { name: /User/i }));
await user.click(screen.getByRole("button", { name: /create user/i }));
await waitFor(() => {
@@ -202,7 +202,7 @@ describe("CreateUserButton", () => {
await user.type(screen.getByLabelText(/user email/i), "info@example.com");
await user.click(screen.getByRole("combobox", { name: /user role/i }));
- await user.click(screen.getByText("User"));
+ await user.click(screen.getByRole("option", { name: /User/i }));
await user.click(screen.getByRole("button", { name: /create user/i }));
await waitFor(() => {
@@ -233,7 +233,7 @@ describe("CreateUserButton", () => {
const dialog = screen.getByRole("dialog", { name: /invite user/i });
await user.type(within(dialog).getByLabelText(/user email/i), "standalone@example.com");
await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i }));
- await user.click(screen.getByText("User"));
+ await user.click(screen.getByRole("option", { name: /User/i }));
await user.click(within(dialog).getByRole("button", { name: /invite user/i }));
await waitFor(() => {
@@ -262,7 +262,7 @@ describe("CreateUserButton", () => {
const dialog = screen.getByRole("dialog", { name: /invite user/i });
await user.type(within(dialog).getByLabelText(/user email/i), "sso@example.com");
await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i }));
- await user.click(screen.getByText("User"));
+ await user.click(screen.getByRole("option", { name: /User/i }));
await user.click(within(dialog).getByRole("button", { name: /invite user/i }));
await waitFor(() => {
@@ -302,12 +302,12 @@ describe("CreateUserButton", () => {
const dialog = screen.getByRole("dialog", { name: /invite user/i });
await user.type(within(dialog).getByLabelText(/user email/i), "org@example.com");
await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i }));
- await user.click(screen.getByText("User"));
+ await user.click(screen.getByRole("option", { name: /User/i }));
// Select org from the dropdown
const orgSelect = within(dialog).getByRole("combobox", { name: /organization/i });
await user.click(orgSelect);
- await user.click(screen.getByText("My Org (org-1)"));
+ await user.click(screen.getByRole("option", { name: /My Org \(org-1\)/i }));
await user.click(within(dialog).getByRole("button", { name: /invite user/i }));
@@ -345,7 +345,7 @@ describe("CreateUserButton", () => {
const dialog = screen.getByRole("dialog", { name: /invite user/i });
await user.type(within(dialog).getByLabelText(/user email/i), "nomemberadd@example.com");
await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i }));
- await user.click(screen.getByText("User"));
+ await user.click(screen.getByRole("option", { name: /User/i }));
await user.click(within(dialog).getByRole("button", { name: /invite user/i }));
await waitFor(() => {
diff --git a/ui/litellm-dashboard/src/components/CreateUserButton.tsx b/ui/litellm-dashboard/src/components/CreateUserButton.tsx
index 0b3b2927271..c1610a05554 100644
--- a/ui/litellm-dashboard/src/components/CreateUserButton.tsx
+++ b/ui/litellm-dashboard/src/components/CreateUserButton.tsx
@@ -1,10 +1,43 @@
-import { Info as InfoCircleOutlined, UserPlus as UserAddOutlined } from "lucide-react";
-import { useQueryClient } from "@tanstack/react-query";
-// eslint-disable-next-line litellm-ui/no-banned-ui-imports
-import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
-import { Accordion, AccordionBody, AccordionHeader, SelectItem, TextInput } from "@tremor/react";
-import { Alert, Button, Form, Input, Modal, Select, Select as Select2, Space, Tooltip, Typography } from "antd";
import React, { useEffect, useMemo, useState } from "react";
+import { useQueryClient } from "@tanstack/react-query";
+import { Controller, useForm } from "react-hook-form";
+import { Info, UserPlus, X } from "lucide-react";
+
+import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+
import BulkCreateUsers from "./bulk_create_users_button";
import TeamDropdown from "./common_components/team_dropdown";
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
@@ -17,8 +50,7 @@ import {
userCreateCall,
} from "./networking";
import OnboardingModal, { InvitationLink } from "./onboarding_link";
-const { Option } = Select;
-const { Text, Link, Title } = Typography;
+
// Helper function to generate UUID compatible across all environments
const generateUUID = (): string => {
if (typeof crypto !== "undefined" && crypto.randomUUID) {
@@ -49,6 +81,102 @@ interface UISettings {
SSO_ENABLED: boolean;
}
+interface CreateUserFormValues {
+ user_email?: string;
+ user_role: string;
+ team_id?: string;
+ organization_ids?: string[];
+ models?: string[];
+ metadata?: string;
+}
+
+const defaultFormValues: CreateUserFormValues = {
+ user_email: "",
+ user_role: "internal_user_viewer",
+ team_id: undefined,
+ organization_ids: [],
+ models: [],
+ metadata: "",
+};
+
+/**
+ * shadcn Select + badge chip multi-select. Mirrors the pattern from
+ * AccessGroupBaseForm. Used for org IDs and personal model selection.
+ */
+function MultiSelect({
+ value,
+ onChange,
+ options,
+ placeholder,
+ emptyText,
+ ariaLabel,
+}: {
+ value: string[];
+ onChange: (next: string[]) => void;
+ options: { label: string; value: string }[];
+ placeholder: string;
+ emptyText: string;
+ ariaLabel?: string;
+}) {
+ const selected = useMemo(() => value ?? [], [value]);
+ const remaining = useMemo(
+ () => options.filter((o) => !selected.includes(o.value)),
+ [options, selected],
+ );
+
+ return (
+
+
{
+ if (v) onChange([...selected, v]);
+ }}
+ >
+
+
+
+
+ {remaining.length === 0 ? (
+
+ {emptyText}
+
+ ) : (
+ remaining.map((opt) => (
+
+ {opt.label}
+
+ ))
+ )}
+
+
+ {selected.length > 0 && (
+
+ {selected.map((v) => {
+ const opt = options.find((o) => o.value === v);
+ return (
+
+ {opt?.label ?? v}
+ onChange(selected.filter((s) => s !== v))}
+ className="inline-flex items-center justify-center rounded-full hover:bg-muted-foreground/20"
+ aria-label={`Remove ${opt?.label ?? v}`}
+ >
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
export const CreateUserButton: React.FC = ({
userID,
accessToken,
@@ -59,16 +187,26 @@ export const CreateUserButton: React.FC = ({
}) => {
const queryClient = useQueryClient();
const [uiSettings, setUISettings] = useState(null);
- const [form] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false);
const [apiuser, setApiuser] = useState(false);
const [userModels, setUserModels] = useState([]);
- const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false);
- const [invitationLinkData, setInvitationLinkData] = useState(null);
+ const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] =
+ useState(false);
+ const [invitationLinkData, setInvitationLinkData] =
+ useState(null);
const [baseUrl, setBaseUrl] = useState(null);
const { data: organizations = [] } = useOrganizations();
+ const {
+ control,
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors },
+ } = useForm({ defaultValues: defaultFormValues });
+
// Derive teams from the user's organizations, falling back to the teams prop
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
const availableTeams = useMemo(() => {
const orgTeams = organizations.flatMap((org) => org.teams || []);
if (orgTeams.length > 0) return orgTeams;
@@ -79,7 +217,11 @@ export const CreateUserButton: React.FC = ({
const fetchData = async () => {
try {
const userRole = "any";
- const modelDataResponse = await modelAvailableCall(accessToken, userID, userRole);
+ const modelDataResponse = await modelAvailableCall(
+ accessToken,
+ userID,
+ userRole,
+ );
const availableModels = [];
for (let i = 0; i < modelDataResponse.data.length; i++) {
const model = modelDataResponse.data[i];
@@ -95,46 +237,47 @@ export const CreateUserButton: React.FC = ({
setBaseUrl(getProxyBaseUrl());
fetchData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- const handleOk = () => {
- setIsModalVisible(false);
- form.resetFields();
- };
-
const handleCancel = () => {
setIsModalVisible(false);
setApiuser(false);
- form.resetFields();
+ reset(defaultFormValues);
};
- const handleCreate = async (formValues: {
- user_id: string;
- models?: string[];
- user_role: string;
- organization_ids?: string[];
- organizations?: string[];
- }) => {
+ const handleCreate = async (formValues: CreateUserFormValues) => {
try {
NotificationsManager.info("Making API Call");
if (!isEmbedded) {
setIsModalVisible(true);
}
- if ((!formValues.models || formValues.models.length === 0) && formValues.user_role !== "proxy_admin") {
- formValues.models = ["no-default-models"];
+
+ const payload: Record = {
+ user_email: formValues.user_email,
+ user_role: formValues.user_role,
+ team_id: formValues.team_id,
+ models: formValues.models,
+ metadata: formValues.metadata,
+ };
+
+ if (
+ (!payload.models || payload.models.length === 0) &&
+ payload.user_role !== "proxy_admin"
+ ) {
+ payload.models = ["no-default-models"];
}
- if (formValues.organization_ids) {
- formValues.organizations = formValues.organization_ids;
- delete formValues.organization_ids;
+ if (formValues.organization_ids && formValues.organization_ids.length > 0) {
+ payload.organizations = formValues.organization_ids;
}
- const response = await userCreateCall(accessToken, null, formValues);
+ const response = await userCreateCall(accessToken, null, payload);
await queryClient.invalidateQueries({ queryKey: ["userList"] });
setApiuser(true);
const user_id = response.data?.user_id || response.user_id;
if (onUserCreated && isEmbedded) {
onUserCreated(user_id);
- form.resetFields();
+ reset(defaultFormValues);
return;
}
@@ -148,13 +291,13 @@ export const CreateUserButton: React.FC = ({
// create an InvitationLink Object for this user for the SSO flow
// for SSO the invite link is the proxy base url since the User just needs to login
const invitationLink: InvitationLink = {
- id: generateUUID(), // Generate a unique ID
+ id: generateUUID(),
user_id: user_id,
is_accepted: false,
accepted_at: null,
- expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // Set expiry to 7 days from now
+ expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
created_at: new Date(),
- created_by: userID, // Assuming userID is the current user creating the invitation
+ created_by: userID,
updated_at: new Date(),
updated_by: userID,
has_user_setup_sso: true,
@@ -164,210 +307,322 @@ export const CreateUserButton: React.FC = ({
}
NotificationsManager.success("API user Created");
- form.resetFields();
+ reset(defaultFormValues);
localStorage.removeItem("userData" + userID);
} catch (error: any) {
- const errorMessage = error.response?.data?.detail || error?.message || "Error creating the user";
+ const errorMessage =
+ error.response?.data?.detail || error?.message || "Error creating the user";
NotificationsManager.fromBackend(errorMessage);
console.error("Error creating the user:", error);
}
};
+ const orgOptions = organizations.map((org) => ({
+ label: `${org.organization_alias} (${org.organization_id})`,
+ value: org.organization_id,
+ }));
+
+ const modelOptions = useMemo(() => {
+ const base = [
+ { label: "All Proxy Models", value: "all-proxy-models" },
+ { label: "No Default Models", value: "no-default-models" },
+ ];
+ const extras = userModels.map((model) => ({
+ label: getModelDisplayName(model),
+ value: model,
+ }));
+ return [...base, ...extras];
+ }, [userModels]);
+
// Modify the return statement to handle embedded mode
if (isEmbedded) {
return (
-