diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx deleted file mode 100644 index be3ea48bf2..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx +++ /dev/null @@ -1,346 +0,0 @@ -"use client"; - -// TODO: refactor -import React, { useState, useEffect } from "react"; -import { Button, Modal, Form, Input, message, Select, InputNumber, Select as Select2 } from "antd"; -import { - Button as Button2, - Text, - TextInput, - SelectItem, - Accordion, - AccordionHeader, - AccordionBody, - Title, -} from "@tremor/react"; -import NotificationsManager from "@/components/molecules/notifications_manager"; -const { Option } = Select; -import { Tooltip } from "antd"; -import { InfoCircleOutlined } from "@ant-design/icons"; -import { useQueryClient } from "@tanstack/react-query"; -import OnboardingModal, { InvitationLink } from "@/components/onboarding_link"; -import { - getProxyBaseUrl, - getProxyUISettings, - invitationCreateCall, - modelAvailableCall, - Team, - userCreateCall, -} from "@/components/networking"; -import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; -import BulkCreateUsersButton from "@/components/bulk_create_users_button"; -import useTeams from "@/app/(dashboard)/hooks/useTeams"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -// Helper function to generate UUID compatible across all environments -const generateUUID = (): string => { - if (typeof crypto !== "undefined" && crypto.randomUUID) { - return crypto.randomUUID(); - } - // Fallback UUID generation for environments without crypto.randomUUID - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0; - const v = c == "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -}; - -interface CreateUserModalProps { - possibleUIRoles: null | Record>; - onUserCreated?: (userId: string) => void; - isEmbedded?: boolean; -} - -interface UISettings { - PROXY_BASE_URL: string | null; - PROXY_LOGOUT_URL: string | null; - DEFAULT_TEAM_DISABLED: boolean; - SSO_ENABLED: boolean; -} - -const CreateUserModal: React.FC = ({ possibleUIRoles, onUserCreated, isEmbedded = false }) => { - const { userId: userID, accessToken } = useAuthorized(); - 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 [baseUrl, setBaseUrl] = useState(null); - const { teams } = useTeams(); - - // get all models - useEffect(() => { - const fetchData = async () => { - try { - const userRole = "any"; // You may need to get the user role dynamically - const modelDataResponse = await modelAvailableCall(accessToken, userID, userRole); - // Assuming modelDataResponse.data contains an array of model objects with a 'model_name' property - const availableModels = []; - for (let i = 0; i < modelDataResponse.data.length; i++) { - const model = modelDataResponse.data[i]; - availableModels.push(model.id); - } - console.log("Model data response:", modelDataResponse.data); - console.log("Available models:", availableModels); - - // Assuming modelDataResponse.data contains an array of model names - setUserModels(availableModels); - - // get ui settings - const uiSettingsResponse = await getProxyUISettings(accessToken); - console.log("uiSettingsResponse:", uiSettingsResponse); - - setUISettings(uiSettingsResponse); - } catch (error) { - console.error("Error fetching model data:", error); - } - }; - - setBaseUrl(getProxyBaseUrl()); - - fetchData(); // Call the function to fetch model data when the component mounts - }, []); // Empty dependency array to run only once - - const handleOk = () => { - setIsModalVisible(false); - form.resetFields(); - }; - - const handleCancel = () => { - setIsModalVisible(false); - setApiuser(false); - form.resetFields(); - }; - - const handleCreate = async (formValues: { user_id: string; models?: string[]; user_role: string }) => { - try { - NotificationsManager.info("Making API Call"); - if (!isEmbedded) { - setIsModalVisible(true); - } - if ((!formValues.models || formValues.models.length === 0) && formValues.user_role !== "proxy_admin") { - console.log("formValues.user_role", formValues.user_role); - // If models is empty or undefined, set it to "no-default-models" - formValues.models = ["no-default-models"]; - } - console.log("formValues in create user:", formValues); - const response = await userCreateCall(accessToken, null, formValues); - await queryClient.invalidateQueries({ queryKey: ["userList"] }); - console.log("user create Response:", response); - setApiuser(true); - const user_id = response.data?.user_id || response.user_id; - - // Call the callback if provided (for embedded mode) - if (onUserCreated && isEmbedded) { - onUserCreated(user_id); - form.resetFields(); - return; // Skip the invitation flow when embedded - } - - // only do invite link flow if sso is not enabled - if (!uiSettings?.SSO_ENABLED) { - invitationCreateCall(accessToken, user_id).then((data) => { - data.has_user_setup_sso = false; - setInvitationLinkData(data); - setIsInvitationLinkModalVisible(true); - }); - } else { - // 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 - 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 - created_at: new Date(), - created_by: userID, // Assuming userID is the current user creating the invitation - updated_at: new Date(), - updated_by: userID, - has_user_setup_sso: true, - }; - setInvitationLinkData(invitationLink); - setIsInvitationLinkModalVisible(true); - } - - NotificationsManager.success("API user Created"); - form.resetFields(); - localStorage.removeItem("userData" + userID); - } catch (error: any) { - const errorMessage = error.response?.data?.detail || error?.message || "Error creating the user"; - NotificationsManager.fromBackend(errorMessage); - console.error("Error creating the user:", error); - } - }; - - // Modify the return statement to handle embedded mode - if (isEmbedded) { - return ( -
- - - - - - {possibleUIRoles && - Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => ( - -
- {ui_label}{" "} -

- {description} -

-
-
- ))} -
-
- - - - - - - - -
- -
-
- ); - } - - // Original return for standalone mode - return ( -
- setIsModalVisible(true)}> - + Invite User - - - - Create a User who can own keys -
- - - - - Global Proxy Role{" "} - - - - - } - name="user_role" - > - - {possibleUIRoles && - Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => ( - -
- {ui_label}{" "} -

- {description} -

-
-
- ))} -
-
- - - - - - - - - - - Personal Key Creation - - - - Models{" "} - - - - - } - name="models" - help="Models user has access to, outside of team scope." - > - - - All Proxy Models - - {userModels.map((model) => ( - - {getModelDisplayName(model)} - - ))} - - - - -
- -
-
-
- {apiuser && ( - - )} -
- ); -}; - -export default CreateUserModal; diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index a0d8d162c6..e47691d3bb 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -1,4 +1,8 @@ import { Layout, Menu } from "antd"; +import Link from "next/link"; +import { List } from "postcss/lib/list"; +import { Text, Button } from "@tremor/react"; +import { useState } from "react"; import { KeyOutlined, PlayCircleOutlined, @@ -56,17 +60,6 @@ interface MenuItem { icon?: React.ReactNode; } -/** ---------- Base URL helpers ---------- */ -/** - * Normalizes NEXT_PUBLIC_BASE_URL to either "/" or "/ui/" (always with a trailing slash). - * Supported env values: "" or "ui/". - */ -const getBasePath = () => { - const raw = process.env.NEXT_PUBLIC_BASE_URL ?? ""; - const trimmed = raw.replace(/^\/+|\/+$/g, ""); // strip leading/trailing slashes - return trimmed ? `/${trimmed}/` : "/"; // ensure trailing slash -}; - const Sidebar: React.FC = ({ accessToken, setPage, userRole, defaultSelectedKey, collapsed = false }) => { // Note: If a menu item does not have a role, it is visible to all roles. const menuItems: MenuItem[] = [ @@ -225,7 +218,6 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau ], }, ]; - // Find the menu item that matches the default page, including in submenus const findMenuItemKey = (page: string): string => { // Check top-level items @@ -260,15 +252,6 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau return true; }); - // Centralized navigation that prefixes the base path ("/" or "/ui/") - const goTo = (page: string) => { - const base = getBasePath(); // "/" or "/ui/" - const newSearchParams = new URLSearchParams(window.location.search); - newSearchParams.set("page", page); - window.history.pushState(null, "", `${base}?${newSearchParams.toString()}`); // e.g. "/ui/?page=..." - setPage(page); - }; - return ( = ({ accessToken, setPage, userRole, defau key: child.key, icon: child.icon, label: child.label, - onClick: () => goTo(child.page), + onClick: () => { + const newSearchParams = new URLSearchParams(window.location.search); + newSearchParams.set("page", child.page); + window.history.pushState(null, "", `?${newSearchParams.toString()}`); + setPage(child.page); + }, })), - onClick: !item.children ? () => goTo(item.page) : undefined, + onClick: !item.children + ? () => { + const newSearchParams = new URLSearchParams(window.location.search); + newSearchParams.set("page", item.page); + window.history.pushState(null, "", `?${newSearchParams.toString()}`); + setPage(item.page); + } + : undefined, }))} />