diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index c879eea3687..2436cb0e3b7 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1471,14 +1471,6 @@ "count": 1 } }, - "src/app/(dashboard)/users/_components/edit_user.tsx": { - "local/filename-pascal-case": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/users/_components/index.tsx": { "local/filename-pascal-case": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/edit_user.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/edit_user.test.tsx deleted file mode 100644 index fd709bbdbe9..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/edit_user.test.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import React from "react"; -import { describe, expect, it, vi } from "vitest"; -import { screen, waitFor } from "@testing-library/react"; -import userEvent, { PointerEventsCheckLevel } from "@testing-library/user-event"; -import { renderWithProviders } from "@/../tests/test-utils"; -import EditUserModal from "./edit_user"; - -const POSSIBLE_UI_ROLES = { - proxy_admin: { ui_label: "Admin", description: "Can create keys, teams, users" }, - internal_user: { ui_label: "Internal User", description: "Can create keys for themselves" }, -}; - -const USER = { - user_id: "user-123", - user_email: "seed@example.com", - user_role: "internal_user", - spend: 3.5, - max_budget: 10, - budget_duration: "24h", - created_at: "2026-01-01T00:00:00Z", - updated_at: "2026-01-02T00:00:00Z", - teams: ["team-a"], - models: ["gpt-4"], - key_count: 7, -}; - -const renderModal = (overrides: Partial> = {}) => { - const onSubmit = vi.fn(); - const onCancel = vi.fn(); - renderWithProviders( - , - ); - return { onSubmit, onCancel }; -}; - -const save = async (user: ReturnType) => { - const buttons = screen.getAllByRole("button", { name: "Save" }); - await user.click(buttons[0]); -}; - -describe("EditUserModal", () => { - it("renders nothing when there is no user", () => { - renderWithProviders( - , - ); - expect(screen.queryByText(/Edit User/)).not.toBeInTheDocument(); - }); - - it("titles the modal with the user id", async () => { - renderModal(); - expect(await screen.findByText("Edit User user-123")).toBeInTheDocument(); - }); - - it("submits exactly the six bound fields, seeded from the user, and drops every other user key", async () => { - const user = userEvent.setup(); - const { onSubmit, onCancel } = renderModal(); - await screen.findByText("Edit User user-123"); - - await save(user); - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - const payload = onSubmit.mock.calls[0][0]; - expect(Object.keys(payload).sort()).toEqual([ - "budget_duration", - "max_budget", - "spend", - "user_email", - "user_id", - "user_role", - ]); - const seededPayload = { - user_id: "user-123", - user_email: "seed@example.com", - user_role: "internal_user", - spend: 3.5, - max_budget: 10, - budget_duration: "24h", - }; - expect(payload).toEqual(seededPayload); - expect(onCancel).toHaveBeenCalledTimes(1); - }); - - it("submits the edited email as a string", async () => { - const user = userEvent.setup(); - const { onSubmit } = renderModal(); - const email = await screen.findByLabelText("User Email"); - await user.clear(email); - await user.type(email, "edited@example.com"); - - await save(user); - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - expect(onSubmit.mock.calls[0][0].user_email).toBe("edited@example.com"); - }); - - it("submits spend as a number and max_budget as a string once both are retyped", async () => { - const user = userEvent.setup(); - const { onSubmit } = renderModal(); - const spend = await screen.findByLabelText("Spend (USD)"); - await user.clear(spend); - await user.type(spend, "42.567"); - const maxBudget = screen.getByLabelText("User Budget (USD)"); - await user.clear(maxBudget); - await user.type(maxBudget, "77.25"); - - await save(user); - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - const payload = onSubmit.mock.calls[0][0]; - expect(payload.spend).toBe(42.567); - expect(payload.max_budget).toBe("77.25"); - }); - - it("keeps a cleared spend and a cleared max_budget distinguishable from zero", async () => { - const user = userEvent.setup(); - const { onSubmit } = renderModal(); - const spend = await screen.findByLabelText("Spend (USD)"); - await user.clear(spend); - const maxBudget = screen.getByLabelText("User Budget (USD)"); - await user.clear(maxBudget); - - await save(user); - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - const payload = onSubmit.mock.calls[0][0]; - expect(payload.spend).toBeNull(); - expect(payload.max_budget).toBe(""); - }); - - it("clamps a negative spend up to the minimum on blur", async () => { - const user = userEvent.setup(); - const { onSubmit } = renderModal(); - const spend = await screen.findByLabelText("Spend (USD)"); - await user.clear(spend); - await user.type(spend, "-5"); - - await save(user); - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - expect(onSubmit.mock.calls[0][0].spend).toBe(0); - }); - - it("blocks the whole submit while max_budget is below its minimum", async () => { - const user = userEvent.setup(); - const { onSubmit, onCancel } = renderModal(); - const maxBudget = await screen.findByLabelText("User Budget (USD)"); - await user.clear(maxBudget); - await user.type(maxBudget, "-5"); - - await save(user); - - expect(onSubmit).not.toHaveBeenCalled(); - expect(onCancel).not.toHaveBeenCalled(); - }); - - it("submits the selected role value, not its label", async () => { - const user = userEvent.setup({ pointerEventsCheck: PointerEventsCheckLevel.Never }); - const { onSubmit } = renderModal(); - await screen.findByText("Edit User user-123"); - await user.click(screen.getByLabelText("User Role")); - await user.click(await screen.findByTitle("Admin")); - - await save(user); - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - expect(onSubmit.mock.calls[0][0].user_role).toBe("proxy_admin"); - }); - - it("submits the selected budget duration code", async () => { - const user = userEvent.setup({ pointerEventsCheck: PointerEventsCheckLevel.Never }); - const { onSubmit } = renderModal(); - await screen.findByText("Edit User user-123"); - await user.click(screen.getByLabelText("Reset Budget")); - await user.click(await screen.findByRole("option", { name: "weekly" })); - - await save(user); - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - expect(onSubmit.mock.calls[0][0].budget_duration).toBe("7d"); - }); - - it("does not submit when the user cancels", async () => { - const user = userEvent.setup(); - const { onSubmit, onCancel } = renderModal(); - await screen.findByText("Edit User user-123"); - await user.click(screen.getByRole("button", { name: /close/i })); - expect(onSubmit).not.toHaveBeenCalled(); - await waitFor(() => expect(onCancel).toHaveBeenCalledTimes(1)); - }); - - it("forwards null fields from the loaded user unchanged", async () => { - const actor = userEvent.setup({ pointerEventsCheck: PointerEventsCheckLevel.Never }); - const { onSubmit } = renderModal({ user: { ...USER, spend: null, max_budget: null, budget_duration: null } }); - - await save(actor); - - const nulledPayload = { - user_email: "seed@example.com", - user_id: "user-123", - user_role: "internal_user", - spend: null, - max_budget: null, - budget_duration: null, - }; - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - expect(onSubmit.mock.calls[0]?.[0]).toEqual(nulledPayload); - }); -}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/edit_user.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/edit_user.tsx deleted file mode 100644 index b11cb10d07a..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/edit_user.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import React from "react"; -import { useForm } from "react-hook-form"; -import { Modal } from "antd"; -import { CircleHelp } from "lucide-react"; - -import NumericalInput from "@/components/shared/numerical_input"; -import BudgetDurationDropdown from "@/components/common_components/budget_duration_dropdown"; -import { FieldGroup } from "@/components/shared/form/field"; -import { FormField } from "@/components/shared/form/FormField"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; - -interface EditableUser { - user_id: string; - user_email: string; - user_role: string; - spend: number | null; - max_budget: number | null; - budget_duration: string | null; -} - -interface EditUserFormValues { - user_email: string | undefined; - user_id: string | undefined; - user_role: string | undefined; - spend: number | null | undefined; - max_budget: number | string | null | undefined; - budget_duration: string | null | undefined; -} - -interface EditUserModalProps { - visible: boolean; - possibleUIRoles: null | Record>; - onCancel: () => void; - user: EditableUser | null; - onSubmit: (data: EditUserFormValues) => void; -} - -interface EditUserFormProps extends Omit { - user: EditableUser; -} - -const SPEND_MIN = 0; - -const toFormValues = (user: EditableUser): EditUserFormValues => ({ - user_email: user.user_email, - user_id: user.user_id, - user_role: user.user_role, - spend: user.spend, - max_budget: user.max_budget, - budget_duration: user.budget_duration, -}); - -const labelWithHint = (label: string, hint: string): React.ReactNode => ( - <> - {label} - - } /> - {hint} - - -); - -const roleOption = (uiLabel: string, description: string): React.ReactNode => ( -
- {uiLabel}

{description}

-
-); - -const EditUserForm: React.FC = ({ visible, possibleUIRoles, onCancel, user, onSubmit }) => { - const form = useForm({ defaultValues: toFormValues(user) }); - - const handleCancel = async () => { - form.reset(toFormValues(user)); - onCancel(); - }; - - const handleEditSubmit = async (formValues: EditUserFormValues) => { - onSubmit(formValues); - form.reset(toFormValues(user)); - onCancel(); - }; - - const clampSpendToMinimum = () => { - const spend = form.getValues("spend"); - if (typeof spend === "number" && spend < SPEND_MIN) { - form.setValue("spend", SPEND_MIN); - } - }; - - const roleItems: Record = Object.fromEntries( - Object.entries(possibleUIRoles ?? {}).map(([role, { ui_label, description }]) => [ - role, - roleOption(ui_label, description), - ]), - ); - - return ( - - -
- - - {({ ref, value, ...field }) => } - - - - {({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => ( - - )} - - - - {({ ref, value, onChange, onBlur, ...field }) => ( - onChange(event.target.value === "" ? null : event.target.valueAsNumber)} - onBlur={() => { - onBlur(); - clampSpendToMinimum(); - }} - /> - )} - - - - {({ ref: _ref, value, ...field }) => ( - - )} - - - - {({ id, value, onChange }) => } - - - -
- -
- -
- -
-
-
-
- ); -}; - -const EditUserModal: React.FC = ({ user, ...props }) => { - if (!user) { - return null; - } - - return ; -}; - -export default EditUserModal; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx index 05e02dc12c4..7a3133840be 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx @@ -7,18 +7,15 @@ import { CreateUserButton } from "@/components/CreateUserButton"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import EditUserModal from "./edit_user"; import { getPossibleUserRoles, getProxyBaseUrl, invitationCreateCall, userListCall, UserListResponse, - userUpdateUserCall, } from "@/components/networking"; import OnboardingModal, { InvitationLink } from "@/components/onboarding_link"; -import { updateExistingKeys } from "@/utils/dataUtils"; import { DEBOUNCE_WAIT_MS } from "@/utils/debounceConstants"; import { isAdminRole, isProxyAdminRole } from "@/utils/roles"; import { useDebouncedValue } from "@tanstack/react-pacer/debouncer"; @@ -77,8 +74,6 @@ const ViewUserDashboard: React.FC = ({ const [selectedUserId, setSelectedUserId] = useQueryState("user", parseAsString.withOptions({ history: "push" })); const [openInEditMode, setOpenInEditMode] = useState(false); - const [editModalVisible, setEditModalVisible] = useState(false); - const [selectedUser, setSelectedUser] = useState(null); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeletingUser, setIsDeletingUser] = useState(false); const [userToDelete, setUserToDelete] = useState(null); @@ -207,39 +202,6 @@ const ViewUserDashboard: React.FC = ({ setUserToDelete(null); }; - const handleEditCancel = async () => { - setSelectedUser(null); - setEditModalVisible(false); - }; - - const handleEditSubmit = async (editedUser: any) => { - if (!accessToken || !token || !userRole || !userID) { - return; - } - - try { - const response = await userUpdateUserCall(accessToken, editedUser, null); - queryClient.setQueriesData({ queryKey: ["userList"] }, (previousData) => { - if (previousData === undefined) return previousData; - const updatedUsers = previousData.users.map((user) => { - if (user.user_id === response.data.user_id) { - return updateExistingKeys(user, response.data); - } - return user; - }); - - return { ...previousData, users: updatedUsers }; - }); - - toast.success(`User ${editedUser.user_id} updated successfully`); - } catch (error) { - console.error("There was an error updating the user", error); - } - setSelectedUser(null); - setEditModalVisible(false); - // Close the modal - }; - const handleToggleSelectionMode = () => { setSelectionMode(!selectionMode); setRowSelection({}); @@ -438,14 +400,6 @@ const ViewUserDashboard: React.FC = ({ )} {/* Existing Modals */} - -