From 03804cb1d29b1fc930c64089e03f7a08e17156f7 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Sun, 5 Oct 2025 16:01:20 -0700 Subject: [PATCH 01/17] preliminary virtual-keys refactor simplifies handleCreate function for CreateKeyModal moved hooks, components, files where I want them moved virtual-key page to (console) for common layout added layout for refactored console file structure, sidebar wired teams to refactored components fixing type issue wired new top level hook useAuthorized Adds routing to unrefactored pages Update leftnav.tsx Update useAuthorized.ts useAuthorized in virtual-keys/page and deprecation notices prelim VirtualKeyViewPage, removed old comment Delete page.tsx --- .../src/app/(console)/components/Sidebar.tsx | 291 ++++++++ .../components/modals/CreateUserModal.tsx | 345 +++++++++ .../src/app/(console)/hooks/useAuthorized.ts | 23 + .../src/app/(console)/layout.tsx | 46 ++ .../virtual-keys/components/CreateKey.tsx | 108 +++ .../CreateKeyForm/KeyDetailsSection.tsx | 148 ++++ .../CreateKeyForm/OptionalSettingsSection.tsx | 458 ++++++++++++ .../CreateKeyForm/OwnershipSection.tsx | 130 ++++ .../CreateKeyModal/CreateKeyModal.tsx | 233 ++++++ .../components/CreateKeyModal/hooks/index.ts | 5 + .../hooks/useGuardrailsAndPrompts.ts | 24 + .../hooks/useMcpAccessGroups.ts | 21 + .../CreateKeyModal/hooks/useTeamModels.ts | 26 + .../CreateKeyModal/hooks/useUserModels.ts | 21 + .../CreateKeyModal/hooks/useUserSearch.ts | 41 ++ .../components/CreateKeyModal/networking.ts | 84 +++ .../components/CreateKeyModal/types.ts | 17 + .../components/CreateKeyModal/utils.ts | 187 +++++ .../virtual-keys/components/KeyInfoView.tsx | 697 ++++++++++++++++++ .../virtual-keys/components/SaveKeyModal.tsx | 58 ++ .../VirtualKeysTable/VirtualKeysTable.tsx | 666 +++++++++++++++++ .../VirtualKeysTable/hooks/useFilterLogic.ts | 189 +++++ .../(console)/virtual-keys/hooks/useTeams.tsx | 20 + .../app/(console)/virtual-keys/networking.ts | 17 + .../src/app/(console)/virtual-keys/page.tsx | 52 ++ ui/litellm-dashboard/src/app/page.tsx | 2 - .../src/components/leftnav.tsx | 50 +- .../organisms/create_key_button.tsx | 8 + .../components/templates/view_key_table.tsx | 7 + ui/litellm-dashboard/src/utils/roles.ts | 2 +- 30 files changed, 3941 insertions(+), 35 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(console)/components/Sidebar.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/components/modals/CreateUserModal.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/hooks/useAuthorized.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/layout.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKey.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/index.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/networking.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/types.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/utils.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/KeyInfoView.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/SaveKeyModal.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/hooks/useTeams.tsx create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/networking.ts create mode 100644 ui/litellm-dashboard/src/app/(console)/virtual-keys/page.tsx diff --git a/ui/litellm-dashboard/src/app/(console)/components/Sidebar.tsx b/ui/litellm-dashboard/src/app/(console)/components/Sidebar.tsx new file mode 100644 index 0000000000..ad60ba626a --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/components/Sidebar.tsx @@ -0,0 +1,291 @@ +import React from "react"; +import Link from "next/link"; +import { usePathname, useSearchParams } from "next/navigation"; +import { Layout, Menu, ConfigProvider } from "antd"; +import { + KeyOutlined, + PlayCircleOutlined, + BlockOutlined, + BarChartOutlined, + TeamOutlined, + BankOutlined, + UserOutlined, + SettingOutlined, + ApiOutlined, + AppstoreOutlined, + DatabaseOutlined, + FileTextOutlined, + LineChartOutlined, + SafetyOutlined, + ExperimentOutlined, + ToolOutlined, + TagsOutlined, + BgColorsOutlined, +} from "@ant-design/icons"; +import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "@/utils/roles"; +import UsageIndicator from "@/components/usage_indicator"; + +const { Sider } = Layout; + +interface SidebarProps { + accessToken: string | null; + userRole: string; + /** Used to highlight a menu item when pathname doesn't match (e.g., on non-routed pages) */ + defaultSelectedKey?: string; + collapsed?: boolean; +} + +interface MenuItem { + key: string; + page: string; + label: string; + roles?: string[]; + children?: MenuItem[]; + icon?: React.ReactNode; +} + +const Sidebar: React.FC = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => { + const menuItems: MenuItem[] = [ + { key: "1", page: "api-keys", label: "Virtual Keys", icon: }, + { + key: "3", + page: "llm-playground", + label: "Test Key", + icon: , + roles: rolesWithWriteAccess, + }, + { + key: "2", + page: "models", + label: "Models + Endpoints", + icon: , + roles: rolesWithWriteAccess, + }, + { + key: "12", + page: "new_usage", + label: "Usage", + icon: , + roles: [...all_admin_roles, ...internalUserRoles], + }, + { key: "6", page: "teams", label: "Teams", icon: }, + { + key: "17", + page: "organizations", + label: "Organizations", + icon: , + roles: all_admin_roles, + }, + { + key: "5", + page: "users", + label: "Internal Users", + icon: , + roles: all_admin_roles, + }, + { key: "14", page: "api_ref", label: "API Reference", icon: }, + { key: "16", page: "model-hub-table", label: "Model Hub", icon: }, + { key: "15", page: "logs", label: "Logs", icon: }, + { + key: "11", + page: "guardrails", + label: "Guardrails", + icon: , + roles: all_admin_roles, + }, + { + key: "26", + page: "tools", + label: "Tools", + icon: , + children: [ + { key: "18", page: "mcp-servers", label: "MCP Servers", icon: }, + { + key: "21", + page: "vector-stores", + label: "Vector Stores", + icon: , + roles: all_admin_roles, + }, + ], + }, + { + key: "experimental", + page: "experimental", + label: "Experimental", + icon: , + children: [ + { + key: "9", + page: "caching", + label: "Caching", + icon: , + roles: all_admin_roles, + }, + { + key: "25", + page: "prompts", + label: "Prompts", + icon: , + roles: all_admin_roles, + }, + { + key: "10", + page: "budgets", + label: "Budgets", + icon: , + roles: all_admin_roles, + }, + { + key: "20", + page: "transform-request", + label: "API Playground", + icon: , + roles: [...all_admin_roles, ...internalUserRoles], + }, + { + key: "19", + page: "tag-management", + label: "Tag Management", + icon: , + roles: all_admin_roles, + }, + { key: "4", page: "usage", label: "Old Usage", icon: }, + ], + }, + { + key: "settings", + page: "settings", + label: "Settings", + icon: , + roles: all_admin_roles, + children: [ + { + key: "11", + page: "general-settings", + label: "Router Settings", + icon: , + roles: all_admin_roles, + }, + { + key: "8", + page: "settings", + label: "Logging & Alerts", + icon: , + roles: all_admin_roles, + }, + { + key: "13", + page: "admin-panel", + label: "Admin Settings", + icon: , + roles: all_admin_roles, + }, + { + key: "14", + page: "ui-theme", + label: "UI Theme", + icon: , + roles: all_admin_roles, + }, + ], + }, + ]; + + // Role filtering (preserves original visibility behavior) + const filteredMenuItems: MenuItem[] = menuItems + .filter((item) => !item.roles || item.roles.includes(userRole)) + .map((item) => ({ + ...item, + children: item.children?.filter((child) => !child.roles || child.roles.includes(userRole)), + })); + + // Highlight selection based on pathname or ?page= + const pathname = usePathname(); + const searchParams = useSearchParams(); + const pageParam = searchParams.get("page") || undefined; + + const findMenuItemKey = (page: string): string => { + const top = filteredMenuItems.find((i) => i.page === page); + if (top) return top.key; + for (const i of filteredMenuItems) { + const child = i.children?.find((c) => c.page === page); + if (child) return child.key; + } + return "1"; + }; + + const selectedMenuKey = + pathname === "/virtual-keys" + ? "1" + : pageParam + ? findMenuItemKey(pageParam) + : defaultSelectedKey + ? findMenuItemKey(defaultSelectedKey) + : "1"; + + // Root-only routing helper: always replace everything after the domain + const rootWithPage = (p: string) => ({ pathname: "/", query: { page: p } }); + + // Convert to AntD Menu items: + // - "Virtual Keys" still routes to /virtual-keys + // - All other items (and children) route to "/?page=" + const antdItems = filteredMenuItems.map((item) => { + const isVirtualKeys = item.key === "1"; + const label = isVirtualKeys ? ( + Virtual Keys + ) : ( + {item.label} + ); + + return { + key: item.key, + icon: item.icon, + label, + children: item.children?.map((child) => ({ + key: child.key, + icon: child.icon, + label: {child.label}, + })), + }; + }); + + return ( + + + + + + + {isAdminRole(userRole) && !collapsed && } + + + ); +}; + +export default Sidebar; diff --git a/ui/litellm-dashboard/src/app/(console)/components/modals/CreateUserModal.tsx b/ui/litellm-dashboard/src/app/(console)/components/modals/CreateUserModal.tsx new file mode 100644 index 0000000000..4caaceb662 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/components/modals/CreateUserModal.tsx @@ -0,0 +1,345 @@ +// 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 { fetchTeams } from "@/app/(console)/virtual-keys/networking"; +import useTeams from "@/app/(console)/virtual-keys/hooks/useTeams"; +import useAuthorized from "@/app/(console)/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/app/(console)/hooks/useAuthorized.ts b/ui/litellm-dashboard/src/app/(console)/hooks/useAuthorized.ts new file mode 100644 index 0000000000..45f5ee92ea --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/hooks/useAuthorized.ts @@ -0,0 +1,23 @@ +import { Router } from "next/router"; +import { getCookie } from "@/utils/cookieUtils"; +import { useRouter } from "next/navigation"; +import { jwtDecode } from "jwt-decode"; + +const useAuthorized = () => { + const token = getCookie("token"); + const router = useRouter(); + + if (!token) { + router.replace("/sso/key/generate"); + } + + const decoded = jwtDecode(token as string) as { [key: string]: any }; + const accessToken = decoded.key; + const userId = decoded.user_id; + const userRole = decoded.user_role; + const premiumUser = decoded.premium_user; + + return { accessToken, userId, userRole, premiumUser }; +}; + +export default useAuthorized; diff --git a/ui/litellm-dashboard/src/app/(console)/layout.tsx b/ui/litellm-dashboard/src/app/(console)/layout.tsx new file mode 100644 index 0000000000..cbfb5bdcb5 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/layout.tsx @@ -0,0 +1,46 @@ +"use client"; + +import React from "react"; +import Navbar from "@/components/navbar"; +import { ThemeProvider } from "@/contexts/ThemeContext"; +import Sidebar from "@/app/(console)/components/Sidebar"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +export default function Layout({ children }: { children: React.ReactNode }) { + const { accessToken, userRole } = useAuthorized(); + const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false); + + const toggleSidebar = () => setSidebarCollapsed((v) => !v); + + return ( + +
+ +
+
+ +
+
{children}
+
+
+
+ ); +} diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKey.tsx b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKey.tsx new file mode 100644 index 0000000000..c8e0b1d8ce --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKey.tsx @@ -0,0 +1,108 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Button } from "@tremor/react"; +import { Modal, Form } from "antd"; +import { getPossibleUserRoles, Organization } from "@/components/networking"; +import { rolesWithWriteAccess } from "@/utils/roles"; +import { Team } from "@/components/key_team_helpers/key_list"; +import CreateKeyModal from "@/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyModal"; +import CreateUserModal from "@/app/(console)/components/modals/CreateUserModal"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +interface CreateKeyProps { + team: Team | null; + userRole: string | null; + data: any[] | null; + addKey: (data: any) => void; +} + +interface User { + user_id: string; + user_email: string; + role?: string; +} + +interface UserOption { + label: string; + value: string; + user: User; +} +const CreateKey: React.FC = ({ team, userRole, data, addKey }) => { + const { accessToken } = useAuthorized(); + const [form] = Form.useForm(); + const [isModalVisible, setIsModalVisible] = useState(false); + const [isCreateUserModalVisible, setIsCreateUserModalVisible] = useState(false); + const [newlyCreatedUserId, setNewlyCreatedUserId] = useState(null); + const [possibleUIRoles, setPossibleUIRoles] = useState>>({}); + + useEffect(() => { + const fetchPossibleRoles = async () => { + try { + if (accessToken) { + // Check if roles are cached in session storage + const cachedRoles = sessionStorage.getItem("possibleUserRoles"); + if (cachedRoles) { + setPossibleUIRoles(JSON.parse(cachedRoles)); + } else { + const availableUserRoles = await getPossibleUserRoles(accessToken); + sessionStorage.setItem("possibleUserRoles", JSON.stringify(availableUserRoles)); + setPossibleUIRoles(availableUserRoles); + } + } + } catch (error) { + console.error("Error fetching possible user roles:", error); + } + }; + + fetchPossibleRoles(); + }, [accessToken]); + + // Add a callback function to handle user creation + const handleUserCreated = (userId: string) => { + setNewlyCreatedUserId(userId); + form.setFieldsValue({ user_id: userId }); + setIsCreateUserModalVisible(false); + }; + + const handleUserSelect = (_value: string, option: UserOption): void => { + const selectedUser = option.user; + form.setFieldsValue({ + user_id: selectedUser.user_id, + }); + }; + + return ( +
+ {userRole && rolesWithWriteAccess.includes(userRole) && ( + + )} + + + {isCreateUserModalVisible && ( + setIsCreateUserModalVisible(false)} + footer={null} + width={800} + > + + + )} +
+ ); +}; + +export default CreateKey; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx new file mode 100644 index 0000000000..54e9aaefed --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx @@ -0,0 +1,148 @@ +import { TextInput, Title } from "@tremor/react"; +import { Form, FormInstance, Select, Tooltip } from "antd"; +import { InfoCircleOutlined } from "@ant-design/icons"; +import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; +import React from "react"; + +export interface KeyDetailsSectionProps { + form: FormInstance; + keyOwner: string; + modelsToPick: string[]; + keyType: string; + setKeyType: (keyType: string) => void; +} + +const { Option } = Select; + +const KeyDetailsSection = ({ form, keyOwner, keyType, modelsToPick, setKeyType }: KeyDetailsSectionProps) => { + return ( +
+ Key Details + + {keyOwner === "you" || keyOwner === "another_user" ? "Key Name" : "Service Account ID"}{" "} + + + + + } + name="key_alias" + rules={[ + { + required: true, + message: `Please input a ${keyOwner === "you" ? "key name" : "service account ID"}`, + }, + ]} + help="required" + > + + + + + Models{" "} + + + + + } + name="models" + rules={ + keyType === "management" || keyType === "read_only" + ? [] + : [{ required: true, message: "Please select a model" }] + } + help={ + keyType === "management" || keyType === "read_only" + ? "Models field is disabled for this key type" + : "required" + } + className="mt-4" + > + + + + + Key Type{" "} + + + + + } + name="key_type" + initialValue="default" + className="mt-4" + > + + +
+ ); +}; + +export default KeyDetailsSection; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx new file mode 100644 index 0000000000..c879cdf595 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx @@ -0,0 +1,458 @@ +import { Accordion, AccordionBody, AccordionHeader, Text, Title } from "@tremor/react"; +import { Form, FormInstance, Input, Select, Tooltip } from "antd"; +import { InfoCircleOutlined } from "@ant-design/icons"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; +import NumericalInput from "@/components/shared/numerical_input"; +import BudgetDurationDropdown from "@/components/common_components/budget_duration_dropdown"; +import RateLimitTypeFormItem from "@/components/common_components/RateLimitTypeFormItem"; +import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector"; +import MCPServerSelector from "@/components/mcp_server_management/MCPServerSelector"; +import PremiumLoggingSettings from "@/components/common_components/PremiumLoggingSettings"; +import ModelAliasManager from "@/components/common_components/ModelAliasManager"; +import KeyLifecycleSettings from "@/components/common_components/KeyLifecycleSettings"; +import { proxyBaseUrl } from "@/components/networking"; +import SchemaFormFields from "@/components/common_components/check_openapi_schema"; +import { Team } from "@/components/key_team_helpers/key_list"; +import React from "react"; +import { DefaultOptionType } from "rc-select/lib/Select"; +import { ModelAliases } from "@/app/(console)/virtual-keys/components/CreateKeyModal/types"; + +export interface OptionalSettingsSectionProps { + form: FormInstance; + team: Team | null; + premiumUser: boolean; + guardrails: string[]; + prompts: string[]; + accessToken: string; + predefinedTags: DefaultOptionType[]; + loggingSettings: any; + setLoggingSettings: (settings: any) => void; + disabledCallbacks: string[]; + setDisabledCallbacks: (disabledCallbacks: string[]) => void; + modelAliases: ModelAliases; + setModelAliases: (aliases: ModelAliases) => void; + autoRotationEnabled: boolean; + setAutoRotationEnabled: (enabled: boolean) => void; + rotationInterval: string; + setRotationInterval: (interval: string) => void; +} + +// TODO: break apart this component into smaller sections +const OptionalSettingsSection = ({ + form, + team, + premiumUser, + guardrails, + prompts, + accessToken, + predefinedTags, + loggingSettings, + setLoggingSettings, + disabledCallbacks, + setDisabledCallbacks, + modelAliases, + setModelAliases, + autoRotationEnabled, + setAutoRotationEnabled, + rotationInterval, + setRotationInterval, +}: OptionalSettingsSectionProps) => { + return ( +
+ + + Optional Settings + + + + Max Budget (USD){" "} + + + + + } + name="max_budget" + help={`Budget cannot exceed team max budget: $${team?.max_budget !== null && team?.max_budget !== undefined ? team?.max_budget : "unlimited"}`} + rules={[ + { + validator: async (_, value) => { + if (value && team && team.max_budget !== null && value > team.max_budget) { + throw new Error( + `Budget cannot exceed team max budget: $${formatNumberWithCommas(team.max_budget, 4)}`, + ); + } + }, + }, + ]} + > + + + + Reset Budget{" "} + + + + + } + name="budget_duration" + help={`Team Reset Budget: ${team?.budget_duration !== null && team?.budget_duration !== undefined ? team?.budget_duration : "None"}`} + > + form.setFieldValue("budget_duration", value)} /> + + + Tokens per minute Limit (TPM){" "} + + + + + } + name="tpm_limit" + help={`TPM cannot exceed team TPM limit: ${team?.tpm_limit !== null && team?.tpm_limit !== undefined ? team?.tpm_limit : "unlimited"}`} + rules={[ + { + validator: async (_, value) => { + if (value && team && team.tpm_limit !== null && value > team.tpm_limit) { + throw new Error(`TPM limit cannot exceed team TPM limit: ${team.tpm_limit}`); + } + }, + }, + ]} + > + + + + + Requests per minute Limit (RPM){" "} + + + + + } + name="rpm_limit" + help={`RPM cannot exceed team RPM limit: ${team?.rpm_limit !== null && team?.rpm_limit !== undefined ? team?.rpm_limit : "unlimited"}`} + rules={[ + { + validator: async (_, value) => { + if (value && team && team.rpm_limit !== null && value > team.rpm_limit) { + throw new Error(`RPM limit cannot exceed team RPM limit: ${team.rpm_limit}`); + } + }, + }, + ]} + > + + + + + Guardrails{" "} + + e.stopPropagation()} // Prevent accordion from collapsing when clicking link + > + + + + + } + name="guardrails" + className="mt-4" + help={ + premiumUser + ? "Select existing guardrails or enter new ones" + : "Premium feature - Upgrade to set guardrails by key" + } + > + ({ value: name, label: name }))} + /> + + + Allowed Vector Stores{" "} + + + + + } + name="allowed_vector_store_ids" + className="mt-4" + help="Select vector stores this key can access. Leave empty for access to all vector stores" + > + form.setFieldValue("allowed_vector_store_ids", values)} + value={form.getFieldValue("allowed_vector_store_ids")} + accessToken={accessToken} + placeholder="Select vector stores (optional)" + /> + + + + Allowed MCP Servers{" "} + + + + + } + name="allowed_mcp_servers_and_groups" + className="mt-4" + help="Select MCP servers or access groups this key can access. " + > + form.setFieldValue("allowed_mcp_servers_and_groups", val)} + value={form.getFieldValue("allowed_mcp_servers_and_groups")} + accessToken={accessToken} + placeholder="Select MCP servers or access groups (optional)" + /> + + + + Metadata{" "} + + + + + } + name="metadata" + className="mt-4" + > + + + + Tags{" "} + + + + + } + name="tags" + className="mt-4" + help={`Tags for tracking spend and/or doing tag-based routing.`} + > + handleUserSelect(value, option as UserOption)} + options={userOptions} + loading={userSearchLoading} + allowClear + style={{ width: "100%" }} + notFoundContent={userSearchLoading ? "Searching..." : "No users found"} + /> + setIsCreateUserModalVisible(true)} style={{ marginLeft: "8px" }}> + Create User + +
+
Search by email to find users
+ + + )} + + Team{" "} + + + + + } + name="team_id" + initialValue={team ? team.team_id : null} + className="mt-4" + rules={[ + { + required: keyOwner === "service_account", + message: "Please select a team for the service account", + }, + ]} + help={keyOwner === "service_account" ? "required" : ""} + > + { + const selectedTeam = teams?.find((t) => t.team_id === teamId) || null; + setSelectedCreateKeyTeam(selectedTeam); + }} + /> + + + ); +}; + +export default OwnershipSection; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx new file mode 100644 index 0000000000..9f4d7528a8 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx @@ -0,0 +1,233 @@ +import { Button as Button2, Form, FormInstance, Modal } from "antd"; +import { Text } from "@tremor/react"; +import { keyCreateCall, keyCreateServiceAccountCall } from "@/components/networking"; +import React, { useEffect, useState } from "react"; +import { Team } from "@/components/key_team_helpers/key_list"; +import SaveKeyModal from "@/app/(console)/virtual-keys/components/SaveKeyModal"; +import NotificationsManager from "@/components/molecules/notifications_manager"; +import { + useGuardrailsAndPrompts, + useMcpAccessGroups, + useTeamModels, + useUserModels, + useUserSearch, +} from "@/app/(console)/virtual-keys/components/CreateKeyModal/hooks"; +import OwnershipSection from "@/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection"; +import KeyDetailsSection from "@/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection"; +import OptionalSettingsSection from "@/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection"; +import { ModelAliases } from "@/app/(console)/virtual-keys/components/CreateKeyModal/types"; +import { getPredefinedTags, prepareFormValues } from "@/app/(console)/virtual-keys/components/CreateKeyModal/utils"; +import { fetchTeams } from "@/app/(console)/virtual-keys/networking"; +import useTeams from "@/app/(console)/virtual-keys/hooks/useTeams"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +export interface CreateKeyModalProps { + isModalVisible: boolean; + setIsModalVisible: (isModalVisible: boolean) => void; + form: FormInstance; + handleUserSelect: (_value: string, option: UserOption) => void; + setIsCreateUserModalVisible: (visible: boolean) => void; + team: Team | null; + data: any[] | null; + addKey: (data: any) => void; +} + +interface User { + user_id: string; + user_email: string; + role?: string; +} + +interface UserOption { + label: string; + value: string; + user: User; +} + +const CreateKeyModal = ({ + isModalVisible, + setIsModalVisible, + form, + handleUserSelect, + setIsCreateUserModalVisible, + team, + data, + addKey, +}: CreateKeyModalProps) => { + const { userId: userID, userRole, accessToken, premiumUser } = useAuthorized(); + const [apiKey, setApiKey] = useState(null); + const [keyOwner, setKeyOwner] = useState("you"); + const [keyType, setKeyType] = useState("default"); + + const [selectedCreateKeyTeam, setSelectedCreateKeyTeam] = useState(team); + + const [softBudget, setSoftBudget] = useState(null); + const [loggingSettings, setLoggingSettings] = useState([]); + const [disabledCallbacks, setDisabledCallbacks] = useState([]); + const [autoRotationEnabled, setAutoRotationEnabled] = useState(false); + const [rotationInterval, setRotationInterval] = useState("30d"); + const [modelAliases, setModelAliases] = useState({}); + const predefinedTags = getPredefinedTags(data); + + const { options: userOptions, loading: userSearchLoading, onSearch: handleUserSearch } = useUserSearch(); + const { guardrails, prompts } = useGuardrailsAndPrompts(); + const teams = useTeams(); + const mcpAccessGroups = useMcpAccessGroups(); + const userModels = useUserModels(); + const modelsToPick = useTeamModels(selectedCreateKeyTeam); + + const isTeamSelectionRequired = modelsToPick.includes("no-default-models"); + const isFormDisabled = isTeamSelectionRequired && !selectedCreateKeyTeam; + + const handleCancel = () => { + setIsModalVisible(false); + setApiKey(null); + setSelectedCreateKeyTeam(null); + form.resetFields(); + setLoggingSettings([]); + setDisabledCallbacks([]); + setKeyType("default"); + setModelAliases({}); + setAutoRotationEnabled(false); + setRotationInterval("30d"); + }; + + const handleOk = () => { + setIsModalVisible(false); + form.resetFields(); + setLoggingSettings([]); + setDisabledCallbacks([]); + setKeyType("default"); + setModelAliases({}); + setAutoRotationEnabled(false); + setRotationInterval("30d"); + }; + + const handleCreate = async (formValues: Record) => { + try { + const newKeyAlias = formValues?.key_alias ?? ""; + const newKeyTeamId = formValues?.team_id ?? null; + + const existingKeyAliases = data?.filter((k) => k.team_id === newKeyTeamId).map((k) => k.key_alias) ?? []; + + if (existingKeyAliases.includes(newKeyAlias)) { + throw new Error( + `Key alias ${newKeyAlias} already exists for team with ID ${newKeyTeamId}, please provide another key alias`, + ); + } + + NotificationsManager.info("Making API Call"); + setIsModalVisible(true); + + const prepared = prepareFormValues(formValues, { + keyOwner, + userID, + loggingSettings, + disabledCallbacks, + autoRotationEnabled, + rotationInterval, + modelAliases, + }); + + let response; + if (keyOwner === "service_account") { + response = await keyCreateServiceAccountCall(accessToken, prepared); + } else { + response = await keyCreateCall(accessToken, userID, prepared); + } + + // TODO: change logic to trigger API call in parent component + addKey(response); + + setApiKey(response["key"]); + setSoftBudget(response["soft_budget"]); + NotificationsManager.success("API Key Created"); + form.resetFields(); + localStorage.removeItem("userData" + userID); + } catch (error) { + console.log("error in create key:", error); + NotificationsManager.fromBackend(`Error creating the key: ${error}`); + } + }; + + useEffect(() => { + form.setFieldValue("models", []); + }, [selectedCreateKeyTeam, accessToken, userID, userRole, form]); + + return ( +
+ +
+ + + {/* Show message when team selection is required */} + {isFormDisabled && ( +
+ + Please select a team to continue configuring your API key. If you do not see any teams, please contact + your Proxy Admin to either provide you with access to models or to add you to a team. + +
+ )} + + {/* Section 2: Key Details */} + {!isFormDisabled && ( + + )} + + {/* Section 3: Optional Settings */} + {!isFormDisabled && ( + + )} + +
+ + Create Key + +
+ +
+ {apiKey && ( + + )} +
+ ); +}; + +export default CreateKeyModal; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/index.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/index.ts new file mode 100644 index 0000000000..ca3c4a663b --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/index.ts @@ -0,0 +1,5 @@ +export * from "./useGuardrailsAndPrompts"; +export * from "./useMcpAccessGroups"; +export * from "./useUserModels"; +export * from "./useTeamModels"; +export * from "./useUserSearch"; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts new file mode 100644 index 0000000000..d092219f70 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; +import { fetchGuardrails, fetchPrompts } from "@/app/(console)/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +export const useGuardrailsAndPrompts = () => { + const [guardrails, setGuardrails] = useState([]); + const [prompts, setPrompts] = useState([]); + const { accessToken } = useAuthorized(); + + useEffect(() => { + if (!accessToken) { + setGuardrails([]); + setPrompts([]); + return; + } + (async () => { + const [g, p] = await Promise.all([fetchGuardrails(accessToken), fetchPrompts(accessToken)]); + setGuardrails(g || []); + setPrompts(p || []); + })(); + }, [accessToken]); + + return { guardrails, prompts }; +}; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts new file mode 100644 index 0000000000..8313f6e8d6 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from "react"; +import { getMCPAccessGroups } from "@/app/(console)/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +export const useMcpAccessGroups = () => { + const [mcpAccessGroups, setMcpAccessGroups] = useState([]); + const { accessToken } = useAuthorized(); + + useEffect(() => { + if (!accessToken) { + setMcpAccessGroups([]); + return; + } + (async () => { + const groups = await getMCPAccessGroups(accessToken); + setMcpAccessGroups(groups || []); + })(); + }, [accessToken]); + + return mcpAccessGroups; +}; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts new file mode 100644 index 0000000000..c28ef0ecdb --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts @@ -0,0 +1,26 @@ +import { useEffect, useMemo, useState } from "react"; +import type { Team } from "@/components/key_team_helpers/key_list"; +import { fetchTeamModels } from "@/app/(console)/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +export const useTeamModels = (selectedTeam: Team | null) => { + const { userId: userID, userRole, accessToken } = useAuthorized(); + const [modelsToPick, setModelsToPick] = useState([]); + + // turn array into a stable dep so it only re-runs when contents change + const selectedTeamModelsKey = useMemo(() => (selectedTeam?.models ?? []).join("|"), [selectedTeam?.models]); + + useEffect(() => { + if (!userID || !userRole || !accessToken) { + setModelsToPick([]); + return; + } + (async () => { + const fetched = await fetchTeamModels(userID, userRole, accessToken, selectedTeam?.team_id ?? null); + const union = Array.from(new Set([...(selectedTeam?.models ?? []), ...(fetched || [])])); + setModelsToPick(union); + })(); + }, [userID, userRole, accessToken, selectedTeam?.team_id, selectedTeamModelsKey, selectedTeam?.models]); + + return modelsToPick; +}; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts new file mode 100644 index 0000000000..ddba6879cc --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from "react"; +import { getUserModelNames } from "@/app/(console)/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +export const useUserModels = () => { + const { userId: userID, userRole, accessToken } = useAuthorized(); + const [userModels, setUserModels] = useState([]); + + useEffect(() => { + if (!userID || !userRole || !accessToken) { + setUserModels([]); + return; + } + (async () => { + const modelNames = await getUserModelNames(userID, userRole, accessToken); + setUserModels(modelNames || []); + })(); + }, [userID, userRole, accessToken]); + + return userModels; +}; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts new file mode 100644 index 0000000000..782beccf48 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts @@ -0,0 +1,41 @@ +import { useEffect, useMemo, useState } from "react"; +import { debounce } from "lodash"; +import { searchUserOptionsByEmail } from "@/app/(console)/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +type User = { user_id: string; user_email: string; role?: string }; +export interface UserOption { + label: string; + value: string; + user: User; +} + +export const useUserSearch = (delay = 300) => { + const { accessToken } = useAuthorized(); + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + + const doSearch = async (raw: string) => { + const q = raw?.trim(); + if (!q || !accessToken) { + setOptions([]); + setLoading(false); + return; + } + setLoading(true); + try { + const res = await searchUserOptionsByEmail(accessToken, q); + setOptions(res || []); + } finally { + setLoading(false); + } + }; + + const onSearch = useMemo(() => debounce(doSearch, delay), [accessToken, delay]); + + useEffect(() => { + return () => onSearch.cancel(); + }, [onSearch]); + + return { options, loading, onSearch }; +}; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/networking.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/networking.ts new file mode 100644 index 0000000000..6cb4e2f7f0 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/networking.ts @@ -0,0 +1,84 @@ +import { + fetchMCPAccessGroups, + getGuardrailsList, + getPromptsList, + modelAvailableCall, + User, + userFilterUICall, +} from "@/components/networking"; +import { ModelAvailableResponse, UserOption } from "@/app/(console)/virtual-keys/components/CreateKeyModal/types"; + +export const fetchGuardrails = async (accessToken: string) => { + try { + const response = await getGuardrailsList(accessToken); + return response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name); + } catch (error) { + console.error("Failed to fetch guardrails:", error); + return []; + } +}; + +export const fetchPrompts = async (accessToken: string) => { + try { + const response = await getPromptsList(accessToken); + return response.prompts.map((prompt) => prompt.prompt_id); + } catch (error) { + console.error("Failed to fetch prompts:", error); + return []; + } +}; + +export const searchUserOptionsByEmail = async (accessToken: string, emailQuery: string): Promise => { + const params = new URLSearchParams(); + params.append("user_email", emailQuery); + + const response = await userFilterUICall(accessToken, params); + const users: User[] = response; + + return users.map((user) => ({ + label: `${user.user_email} (${user.user_id})`, + value: user.user_id, + user, + })); +}; + +export const getUserModelNames = async (userID: string, userRole: string, accessToken: string): Promise => { + const res: ModelAvailableResponse = await modelAvailableCall(accessToken, userID, userRole); + return (res?.data ?? []).map((m) => m.id); +}; + +export const fetchTeamModels = async ( + userID: string, + userRole: string, + accessToken: string, + teamID: string | null, +): Promise => { + try { + if (userID === null || userRole === null) { + return []; + } + + if (accessToken !== null) { + const model_available = await modelAvailableCall(accessToken, userID, userRole, true, teamID, true); + let available_model_names = model_available["data"].map((element: { id: string }) => element.id); + console.log("available_model_names:", available_model_names); + return available_model_names; + } + return []; + } catch (error) { + console.error("Error fetching user models:", error); + return []; + } +}; + +export const getMCPAccessGroups = async (accessToken: string): Promise => { + try { + if (accessToken == null) { + return []; + } + return await fetchMCPAccessGroups(accessToken); + } catch (error) { + console.error("Failed to fetch MCP access groups:", error); + return []; + } +}; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/types.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/types.ts new file mode 100644 index 0000000000..4d48df82d4 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/types.ts @@ -0,0 +1,17 @@ +export interface User { + user_id: string; + user_email: string; + role?: string; +} + +export interface UserOption { + label: string; + value: string; + user: User; +} + +export type Model = { id: string }; + +export type ModelAvailableResponse = { data: Model[] }; + +export type ModelAliases = { [key: string]: string }; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/utils.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/utils.ts new file mode 100644 index 0000000000..066770cf46 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/utils.ts @@ -0,0 +1,187 @@ +import { mapDisplayToInternalNames } from "@/components/callback_info_helpers"; +import { ModelAliases } from "@/app/(console)/virtual-keys/components/CreateKeyModal/types"; + +export const getPredefinedTags = (data: any[] | null) => { + let allTags = []; + + console.log("data:", JSON.stringify(data)); + + if (data) { + for (let key of data) { + if (key["metadata"] && key["metadata"]["tags"]) { + allTags.push(...key["metadata"]["tags"]); + } + } + } + + const uniqueTags = Array.from(new Set(allTags)).map((tag) => ({ + value: tag, + label: tag, + })); + + console.log("uniqueTags:", uniqueTags); + return uniqueTags; +}; + +type AnyObj = Record; + +type PrepareOptions = { + keyOwner: string; + userID: string; + loggingSettings: any[]; + disabledCallbacks: string[]; + autoRotationEnabled: boolean; + rotationInterval: string; + modelAliases: ModelAliases; +}; + +export function safeParseMetadata(metadataStr: string | undefined): AnyObj { + try { + return JSON.parse(metadataStr || "{}"); + } catch (error) { + console.error("Error parsing metadata:", error); + return {}; + } +} + +export function withOwnerAssignments( + values: AnyObj, + { keyOwner, userID }: Pick, +): AnyObj { + // If owned by "you", set user_id + const base = keyOwner === "you" ? { ...values, user_id: userID } : { ...values }; + return base; +} + +export function enrichMetadata( + metadata: AnyObj, + values: AnyObj, + { + keyOwner, + loggingSettings, + disabledCallbacks, + }: Pick, +): AnyObj { + let next = { ...metadata }; + + // If it's a service account, add the service_account_id to the metadata + if (keyOwner === "service_account") { + next = { ...next, service_account_id: values.key_alias }; + } + + // Add logging settings to the metadata + if (Array.isArray(loggingSettings) && loggingSettings.length > 0) { + next = { ...next, logging: loggingSettings.filter((config: any) => config?.callback_name) }; + } + + // Add disabled callbacks to the metadata + if (Array.isArray(disabledCallbacks) && disabledCallbacks.length > 0) { + const mappedDisabledCallbacks = mapDisplayToInternalNames(disabledCallbacks); + next = { ...next, litellm_disabled_callbacks: mappedDisabledCallbacks }; + } + + return next; +} + +export function withAutoRotation( + values: AnyObj, + { autoRotationEnabled, rotationInterval }: Pick, +): AnyObj { + if (!autoRotationEnabled) return { ...values }; + return { ...values, auto_rotate: true, rotation_interval: rotationInterval }; +} + +export function withDurationNoop(values: AnyObj): AnyObj { + // Preserve the original no-op logic: if duration exists, set it to itself + if (values.duration) { + return { ...values, duration: values.duration }; + } + return { ...values }; +} + +export function mergeObjectPermission(values: AnyObj, patch: AnyObj): AnyObj { + const object_permission = { ...(values.object_permission || {}), ...patch }; + return { ...values, object_permission }; +} + +export function withVectorStores(values: AnyObj): AnyObj { + const { allowed_vector_store_ids, ...rest } = values; + if (Array.isArray(allowed_vector_store_ids) && allowed_vector_store_ids.length > 0) { + const patched = mergeObjectPermission(rest, { vector_stores: allowed_vector_store_ids }); + return patched; // original field removed by destructuring + } + return { ...values }; +} + +export function withMcpServersAndGroups(values: AnyObj): AnyObj { + const { allowed_mcp_servers_and_groups, ...rest } = values; + const servers = allowed_mcp_servers_and_groups?.servers; + const accessGroups = allowed_mcp_servers_and_groups?.accessGroups; + + const hasServers = Array.isArray(servers) && servers.length > 0; + const hasAccessGroups = Array.isArray(accessGroups) && accessGroups.length > 0; + + if (!hasServers && !hasAccessGroups) return { ...values }; + + let patched = { ...rest }; + if (hasServers) patched = mergeObjectPermission(patched, { mcp_servers: servers }); + if (hasAccessGroups) patched = mergeObjectPermission(patched, { mcp_access_groups: accessGroups }); + + // original field removed by destructuring + return patched; +} + +export function withMcpAccessGroups(values: AnyObj): AnyObj { + const { allowed_mcp_access_groups, ...rest } = values; + if (Array.isArray(allowed_mcp_access_groups) && allowed_mcp_access_groups.length > 0) { + const patched = mergeObjectPermission(rest, { mcp_access_groups: allowed_mcp_access_groups }); + return patched; // original field removed by destructuring + } + return { ...values }; +} + +function withAliases(values: AnyObj, modelAliases: ModelAliases): AnyObj { + if (modelAliases && Object.keys(modelAliases).length > 0) { + return { ...values, aliases: JSON.stringify(modelAliases) }; + } + return { ...values }; +} + +function withMetadataString(values: AnyObj, metadata: AnyObj): AnyObj { + return { ...values, metadata: JSON.stringify(metadata) }; +} + +export function prepareFormValues(initialValues: AnyObj, opts: PrepareOptions): AnyObj { + // Step 1: assign user id if needed + let values = withOwnerAssignments(initialValues, { keyOwner: opts.keyOwner, userID: opts.userID }); + + // Step 2: metadata parse & enrichment + const parsedMetadata = safeParseMetadata(values.metadata); + const enrichedMetadata = enrichMetadata(parsedMetadata, values, { + keyOwner: opts.keyOwner, + loggingSettings: opts.loggingSettings, + disabledCallbacks: opts.disabledCallbacks, + }); + + // Step 3: auto-rotation + values = withAutoRotation(values, { + autoRotationEnabled: opts.autoRotationEnabled, + rotationInterval: opts.rotationInterval, + }); + + // Step 4: duration (no-op preservation) + values = withDurationNoop(values); + + // Step 5: commit metadata string + values = withMetadataString(values, enrichedMetadata); + + // Step 6: object_permission transformations + values = withVectorStores(values); + values = withMcpServersAndGroups(values); + values = withMcpAccessGroups(values); + + // Step 7: aliases + values = withAliases(values, opts.modelAliases); + + return values; +} diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/KeyInfoView.tsx b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/KeyInfoView.tsx new file mode 100644 index 0000000000..1c7185f1dc --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/KeyInfoView.tsx @@ -0,0 +1,697 @@ +import React, { useEffect, useState } from "react"; +import { + Card, + Text, + Button, + Grid, + Tab, + TabList, + TabGroup, + TabPanel, + TabPanels, + Title, + Badge, +} from "@tremor/react"; +import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline"; +import { Form, Tooltip, Button as AntdButton } from "antd"; +import { copyToClipboard as utilCopyToClipboard, formatNumberWithCommas } from "@/utils/dataUtils"; +import { CopyIcon, CheckIcon } from "lucide-react"; +import { KeyResponse } from "@/components/key_team_helpers/key_list" +import { mapDisplayToInternalNames, mapInternalToDisplayNames } from "@/components/callback_info_helpers" +import NotificationManager from "@/components/molecules/notifications_manager" +import { keyDeleteCall, keyUpdateCall } from "@/components/networking" +import { parseErrorMessage } from "@/components/shared/errorUtils" +import { rolesWithWriteAccess } from "@/utils/roles" +import { RegenerateKeyModal } from "@/components/organisms/regenerate_key_modal" +import ObjectPermissionsView from "@/components/object_permissions_view" +import LoggingSettingsView from "@/components/logging_settings_view" +import { extractLoggingSettings, formatMetadataForDisplay } from "@/components/key_info_utils" +import AutoRotationView from "@/components/common_components/AutoRotationView" +import { KeyEditView } from "@/components/templates/key_edit_view" + + +interface KeyInfoViewProps { + keyId: string; + onClose: () => void; + keyData: KeyResponse | undefined; + onKeyDataUpdate?: (data: Partial) => void; + onDelete?: () => void; + accessToken: string | null; + userID: string | null; + userRole: string | null; + teams: any[] | null; + premiumUser: boolean; + setAccessToken?: (token: string) => void; + backButtonText?: string; +} + +const KeyInfoView = ({ + keyId, + onClose, + keyData, + accessToken, + userID, + userRole, + teams, + onKeyDataUpdate, + onDelete, + premiumUser, + setAccessToken, + backButtonText = "Back to Keys", + }: KeyInfoViewProps) => { + const [isEditing, setIsEditing] = useState(false); + const [form] = Form.useForm(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [deleteConfirmInput, setDeleteConfirmInput] = useState(""); + const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false); + const [copiedStates, setCopiedStates] = useState>({}); + + // Add local state to maintain key data and track regeneration + const [currentKeyData, setCurrentKeyData] = useState(keyData); + const [lastRegeneratedAt, setLastRegeneratedAt] = useState(null); + const [isRecentlyRegenerated, setIsRecentlyRegenerated] = useState(false); + + // Update local state when keyData prop changes (but don't reset to undefined) + useEffect(() => { + if (keyData) { + setCurrentKeyData(keyData); + } + }, [keyData]); + + // Reset recent regeneration indicator after 5 seconds + useEffect(() => { + if (isRecentlyRegenerated) { + const timer = setTimeout(() => { + setIsRecentlyRegenerated(false); + }, 5000); + return () => clearTimeout(timer); + } + }, [isRecentlyRegenerated]); + + // Use currentKeyData instead of keyData throughout the component + if (!currentKeyData) { + return ( +
+ + Key not found +
+ ); + } + + const handleKeyUpdate = async (formValues: Record) => { + try { + if (!accessToken) return; + + const currentKey = formValues.token; + formValues.key = currentKey; + + // Guard premium features + if (!premiumUser) { + delete formValues.guardrails; + delete formValues.prompts; + } + + // Handle object_permission updates + if (formValues.vector_stores !== undefined) { + formValues.object_permission = { + ...currentKeyData.object_permission, + vector_stores: formValues.vector_stores || [], + }; + // Remove vector_stores from the top level as it should be in object_permission + delete formValues.vector_stores; + } + + if (formValues.mcp_servers_and_groups !== undefined) { + const { servers, accessGroups } = formValues.mcp_servers_and_groups || { servers: [], accessGroups: [] }; + formValues.object_permission = { + ...currentKeyData.object_permission, + mcp_servers: servers || [], + mcp_access_groups: accessGroups || [], + }; + // Remove mcp_servers_and_groups from the top level as it should be in object_permission + delete formValues.mcp_servers_and_groups; + } + + // Convert metadata back to an object if it exists and is a string + if (formValues.metadata && typeof formValues.metadata === "string") { + try { + const parsedMetadata = JSON.parse(formValues.metadata); + formValues.metadata = { + ...parsedMetadata, + ...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}), + ...(formValues.logging_settings ? { logging: formValues.logging_settings } : {}), + ...(formValues.disabled_callbacks?.length > 0 + ? { + litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks), + } + : {}), + }; + } catch (error) { + console.error("Error parsing metadata JSON:", error); + NotificationManager.error("Invalid metadata JSON"); + return; + } + } else { + formValues.metadata = { + ...(formValues.metadata || {}), + ...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}), + ...(formValues.logging_settings ? { logging: formValues.logging_settings } : {}), + ...(formValues.disabled_callbacks?.length > 0 + ? { + litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks), + } + : {}), + }; + } + + delete formValues.logging_settings; + + // Convert budget_duration to API format + if (formValues.budget_duration) { + const durationMap: Record = { + daily: "24h", + weekly: "7d", + monthly: "30d", + }; + formValues.budget_duration = durationMap[formValues.budget_duration]; + } + + const newKeyValues = await keyUpdateCall(accessToken, formValues); + + // Update local state + setCurrentKeyData((prevData) => (prevData ? { ...prevData, ...newKeyValues } : undefined)); + + if (onKeyDataUpdate) { + onKeyDataUpdate(newKeyValues); + } + NotificationManager.success("Key updated successfully"); + setIsEditing(false); + // Refresh key data here if needed + } catch (error) { + NotificationManager.fromBackend(parseErrorMessage(error)); + console.error("Error updating key:", error); + } + }; + + const handleDelete = async () => { + try { + if (!accessToken) return; + await keyDeleteCall(accessToken as string, currentKeyData.token || currentKeyData.token_id); + NotificationManager.success("Key deleted successfully"); + if (onDelete) { + onDelete(); + } + onClose(); + } catch (error) { + console.error("Error deleting the key:", error); + NotificationManager.fromBackend(error); + } + // Reset the confirmation input + setDeleteConfirmInput(""); + }; + + const copyToClipboard = async (text: string, key: string) => { + const success = await utilCopyToClipboard(text); + if (success) { + setCopiedStates((prev) => ({ ...prev, [key]: true })); + setTimeout(() => { + setCopiedStates((prev) => ({ ...prev, [key]: false })); + }, 2000); + } + }; + + const handleRegenerateKeyUpdate = (updatedKeyData: Partial) => { + // Update local state immediately with ALL the new data + setCurrentKeyData((prevData) => { + if (!prevData) return undefined; + const newData = { + ...prevData, + ...updatedKeyData, // This should include the new token (key-id) + // Update the created_at to show when it was regenerated + created_at: new Date().toLocaleString(), + }; + return newData; + }); + + // Track regeneration timestamp + setLastRegeneratedAt(new Date()); + setIsRecentlyRegenerated(true); + + if (onKeyDataUpdate) { + onKeyDataUpdate({ + ...updatedKeyData, + created_at: new Date().toLocaleString(), + }); + } + }; + + // Update the formatTimestamp function to use the desired date format + const formatTimestamp = (timestamp: string | Date) => { + const date = new Date(timestamp); + const dateStr = date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + const timeStr = date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + }); + return `${dateStr} at ${timeStr}`; + }; + + return ( +
+
+
+ + {currentKeyData.key_alias || "API Key"} + +
+
+ Key ID + {currentKeyData.token_id || currentKeyData.token} +
+ : } + onClick={() => copyToClipboard(currentKeyData.token_id || currentKeyData.token, "key-id")} + className={`ml-2 transition-all duration-200${ + copiedStates["key-id"] + ? "text-green-600 bg-green-50 border-green-200" + : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" + }`} + /> +
+ + {/* Add timestamp and regeneration indicator */} +
+ + {currentKeyData.updated_at && currentKeyData.updated_at !== currentKeyData.created_at + ? `Updated: ${formatTimestamp(currentKeyData.updated_at)}` + : `Created: ${formatTimestamp(currentKeyData.created_at)}`} + + + {isRecentlyRegenerated && ( + + Recently Regenerated + + )} + + {lastRegeneratedAt && ( + + Regenerated + + )} +
+
+ {userRole && rolesWithWriteAccess.includes(userRole) && ( +
+ + + + + + +
+ )} +
+ + {/* Add RegenerateKeyModal */} + setIsRegenerateModalOpen(false)} + accessToken={accessToken} + premiumUser={premiumUser} + setAccessToken={setAccessToken} + onKeyUpdate={handleRegenerateKeyUpdate} + /> + + {/* Delete Confirmation Modal */} + {isDeleteModalOpen && + (() => { + const keyName = currentKeyData?.key_alias || currentKeyData?.token_id || "API Key"; + const isValid = deleteConfirmInput === keyName; + return ( +
+
+
+
+

Delete Key

+ +
+
+
+
+ + + +
+
+

+ Warning: You are about to delete this API key. +

+

+ This action is irreversible and will immediately revoke access for any applications using this + key. +

+
+
+

Are you sure you want to delete this API key?

+
+ + setDeleteConfirmInput(e.target.value)} + placeholder="Enter key name exactly" + className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base" + autoFocus + /> +
+
+
+
+ + +
+
+
+ ); + })()} + + + + Overview + Settings + + + + {/* Overview Panel */} + + + + Spend +
+ ${formatNumberWithCommas(currentKeyData.spend, 4)} + + of{" "} + {currentKeyData.max_budget !== null + ? `$${formatNumberWithCommas(currentKeyData.max_budget)}` + : "Unlimited"} + +
+
+ + + Rate Limits +
+ TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"} + RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"} +
+
+ + + Models +
+ {currentKeyData.models && currentKeyData.models.length > 0 ? ( + currentKeyData.models.map((model, index) => ( + + {model} + + )) + ) : ( + No models specified + )} +
+
+ + + + + + + + +
+
+ + {/* Settings Panel */} + + +
+ Key Settings + {!isEditing && userRole && rolesWithWriteAccess.includes(userRole) && ( + + )} +
+ + {isEditing ? ( + setIsEditing(false)} + onSubmit={handleKeyUpdate} + teams={teams} + accessToken={accessToken} + userID={userID} + userRole={userRole} + premiumUser={premiumUser} + /> + ) : ( +
+
+ Key ID + {currentKeyData.token_id || currentKeyData.token} +
+ +
+ Key Alias + {currentKeyData.key_alias || "Not Set"} +
+ +
+ Secret Key + {currentKeyData.key_name} +
+ +
+ Team ID + {currentKeyData.team_id || "Not Set"} +
+ +
+ Organization + {currentKeyData.organization_id || "Not Set"} +
+ +
+ Created + {formatTimestamp(currentKeyData.created_at)} +
+ + {lastRegeneratedAt && ( +
+ Last Regenerated +
+ {formatTimestamp(lastRegeneratedAt)} + + Recent + +
+
+ )} + +
+ Expires + {currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never"} +
+ + + +
+ Spend + ${formatNumberWithCommas(currentKeyData.spend, 4)} USD +
+ +
+ Budget + + {currentKeyData.max_budget !== null + ? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}` + : "Unlimited"} + +
+ +
+ Prompts + + {Array.isArray(currentKeyData.metadata?.prompts) && currentKeyData.metadata.prompts.length > 0 + ? currentKeyData.metadata.prompts.map((prompt, index) => ( + + {prompt} + + )) + : "No prompts specified"} + +
+ +
+ Models +
+ {currentKeyData.models && currentKeyData.models.length > 0 ? ( + currentKeyData.models.map((model, index) => ( + + {model} + + )) + ) : ( + No models specified + )} +
+
+ +
+ Rate Limits + TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"} + RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"} + + Max Parallel Requests:{" "} + {currentKeyData.max_parallel_requests !== null + ? currentKeyData.max_parallel_requests + : "Unlimited"} + + + Model TPM Limits:{" "} + {currentKeyData.metadata?.model_tpm_limit + ? JSON.stringify(currentKeyData.metadata.model_tpm_limit) + : "Unlimited"} + + + Model RPM Limits:{" "} + {currentKeyData.metadata?.model_rpm_limit + ? JSON.stringify(currentKeyData.metadata.model_rpm_limit) + : "Unlimited"} + +
+ +
+ Metadata +
+                      {formatMetadataForDisplay(currentKeyData.metadata)}
+                    
+
+ + + + +
+ )} +
+
+
+
+
+ ); +} + +export default KeyInfoView; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/SaveKeyModal.tsx b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/SaveKeyModal.tsx new file mode 100644 index 0000000000..bd0d425c46 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/SaveKeyModal.tsx @@ -0,0 +1,58 @@ +import { Button, Col, Grid, Text, Title } from "@tremor/react"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { Modal } from "antd"; +import React from "react"; +import NotificationsManager from "@/components/molecules/notifications_manager"; + +export interface SaveKeyModalProps { + apiKey: string; + isModalVisible: boolean; + handleOk: () => void; + handleCancel: () => void; +} + +const SaveKeyModal = ({ apiKey, isModalVisible, handleOk, handleCancel }: SaveKeyModalProps) => { + const handleCopy = () => { + NotificationsManager.success("API Key copied to clipboard"); + }; + + return ( + + + Save your Key + +

+ Please save this secret key somewhere safe and accessible. For security reasons,{" "} + you will not be able to view it again through your LiteLLM account. If you lose this secret key, you + will need to generate a new one. +

+ + + {apiKey != null ? ( +
+ API Key: +
+
{apiKey}
+
+ + + + +
+ ) : ( + Key being created, this might take 30s + )} + +
+
+ ); +}; + +export default SaveKeyModal; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx new file mode 100644 index 0000000000..5c32a736ae --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx @@ -0,0 +1,666 @@ +// TODO: refactor +"use client"; + +import React, { useEffect, useState } from "react"; +import { ColumnDef } from "@tanstack/react-table"; +import { Button } from "@tremor/react"; +import { Tooltip } from "antd"; +import { updateExistingKeys } from "@/utils/dataUtils"; +import { flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table"; +import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Icon } from "@tremor/react"; +import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"; +import { Badge, Text } from "@tremor/react"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; +import { Organization, userListCall } from "@/components/networking"; +import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; +import FilterComponent, { FilterOption } from "@/components/molecules/filter"; +import KeyInfoView from "@/components/templates/key_info_view"; +import { useFilterLogic } from "@/components/key_team_helpers/filter_logic"; +import useTeams from "@/app/(console)/virtual-keys/hooks/useTeams"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +interface AllKeysTableProps { + keys: KeyResponse[]; + setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void; + isLoading?: boolean; + pagination: { + currentPage: number; + totalPages: number; + totalCount: number; + }; + onPageChange: (page: number) => void; + pageSize?: number; + organizations: Organization[] | null; + refresh?: () => void; + onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void; + currentSort?: { + sortBy: string; + sortOrder: "asc" | "desc"; + }; + setAccessToken?: (token: string) => void; +} + +interface UserResponse { + user_id: string; + user_email: string; + user_role: string; +} + +const VirtualKeysTable = ({ + keys, + setKeys, + isLoading = false, + pagination, + onPageChange, + pageSize = 50, + organizations, + refresh, + onSortChange, + currentSort, + setAccessToken, +}: AllKeysTableProps) => { + const { userId: userID, userRole, accessToken, premiumUser } = useAuthorized(); + const teams = useTeams(); + const [selectedKeyId, setSelectedKeyId] = useState(null); + const [userList, setUserList] = useState([]); + const [sorting, setSorting] = React.useState(() => { + if (currentSort) { + return [ + { + id: currentSort.sortBy, + desc: currentSort.sortOrder === "desc", + }, + ]; + } + return [ + { + id: "created_at", + desc: true, + }, + ]; + }); + const [expandedAccordions, setExpandedAccordions] = useState>({}); + + // Use the filter logic hook + + const { filters, filteredKeys, allKeyAliases, allTeams, allOrganizations, handleFilterChange, handleFilterReset } = + useFilterLogic({ + keys, + teams, + organizations, + accessToken, + }); + + useEffect(() => { + if (accessToken) { + const user_IDs = keys.map((key) => key.user_id).filter((id) => id !== null); + const fetchUserList = async () => { + const userListData = await userListCall(accessToken, user_IDs, 1, 100); + setUserList(userListData.users); + }; + fetchUserList(); + } + }, [accessToken, keys]); + + // Add a useEffect to call refresh when a key is created + useEffect(() => { + if (refresh) { + const handleStorageChange = () => { + refresh(); + }; + + // Listen for storage events that might indicate a key was created + window.addEventListener("storage", handleStorageChange); + + return () => { + window.removeEventListener("storage", handleStorageChange); + }; + } + }, [refresh]); + + const columns: ColumnDef[] = [ + { + id: "expander", + header: () => null, + cell: ({ row }) => + row.getCanExpand() ? ( + + ) : null, + }, + { + id: "token", + accessorKey: "token", + header: "Key ID", + cell: (info) => ( +
+ + + +
+ ), + }, + { + id: "key_alias", + accessorKey: "key_alias", + header: "Key Alias", + cell: (info) => { + const value = info.getValue() as string; + return ( + {value ? (value.length > 20 ? `${value.slice(0, 20)}...` : value) : "-"} + ); + }, + }, + { + id: "key_name", + accessorKey: "key_name", + header: "Secret Key", + cell: (info) => {info.getValue() as string}, + }, + { + id: "team_alias", + accessorKey: "team_id", + header: "Team Alias", + cell: ({ row, getValue }) => { + const teamId = getValue() as string; + const team = teams?.find((t) => t.team_id === teamId); + return team?.team_alias || "Unknown"; + }, + }, + { + id: "team_id", + accessorKey: "team_id", + header: "Team ID", + cell: (info) => ( + + {info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"} + + ), + }, + { + id: "organization_id", + accessorKey: "organization_id", + header: "Organization ID", + cell: (info) => (info.getValue() ? info.renderValue() : "-"), + }, + { + id: "user_email", + accessorKey: "user_id", + header: "User Email", + cell: (info) => { + const userId = info.getValue() as string; + const user = userList.find((u) => u.user_id === userId); + return user?.user_email ? ( + + {user?.user_email.slice(0, 20)}... + + ) : ( + "-" + ); + }, + }, + { + id: "user_id", + accessorKey: "user_id", + header: "User ID", + cell: (info) => { + const userId = info.getValue() as string | null; + if (userId && userId.length > 15) { + return ( + + {userId.slice(0, 7)}... + + ); + } + return userId ? userId : "-"; + }, + }, + { + id: "created_at", + accessorKey: "created_at", + header: "Created At", + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleDateString() : "-"; + }, + }, + { + id: "created_by", + accessorKey: "created_by", + header: "Created By", + cell: (info) => { + const value = info.getValue() as string | null; + if (value && value.length > 15) { + return ( + + {value.slice(0, 7)}... + + ); + } + return value; + }, + }, + { + id: "updated_at", + accessorKey: "updated_at", + header: "Updated At", + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleDateString() : "Never"; + }, + }, + { + id: "expires", + accessorKey: "expires", + header: "Expires", + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleDateString() : "Never"; + }, + }, + { + id: "spend", + accessorKey: "spend", + header: "Spend (USD)", + cell: (info) => formatNumberWithCommas(info.getValue() as number, 4), + }, + { + id: "max_budget", + accessorKey: "max_budget", + header: "Budget (USD)", + cell: (info) => { + const maxBudget = info.getValue() as number | null; + if (maxBudget === null) { + return "Unlimited"; + } + return `$${formatNumberWithCommas(maxBudget)}`; + }, + }, + { + id: "budget_reset_at", + accessorKey: "budget_reset_at", + header: "Budget Reset", + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleString() : "Never"; + }, + }, + { + id: "models", + accessorKey: "models", + header: "Models", + cell: (info) => { + const models = info.getValue() as string[]; + return ( +
+ {Array.isArray(models) ? ( +
+ {models.length === 0 ? ( + + All Proxy Models + + ) : ( + <> +
+ {models.length > 3 && ( +
+ { + setExpandedAccordions((prev) => ({ + ...prev, + [info.row.id]: !prev[info.row.id], + })); + }} + /> +
+ )} +
+ {models.slice(0, 3).map((model, index) => + model === "all-proxy-models" ? ( + + All Proxy Models + + ) : ( + + + {model.length > 30 + ? `${getModelDisplayName(model).slice(0, 30)}...` + : getModelDisplayName(model)} + + + ), + )} + {models.length > 3 && !expandedAccordions[info.row.id] && ( + + + +{models.length - 3} {models.length - 3 === 1 ? "more model" : "more models"} + + + )} + {expandedAccordions[info.row.id] && ( +
+ {models.slice(3).map((model, index) => + model === "all-proxy-models" ? ( + + All Proxy Models + + ) : ( + + + {model.length > 30 + ? `${getModelDisplayName(model).slice(0, 30)}...` + : getModelDisplayName(model)} + + + ), + )} +
+ )} +
+
+ + )} +
+ ) : null} +
+ ); + }, + }, + { + id: "rate_limits", + header: "Rate Limits", + cell: ({ row }) => { + const key = row.original; + return ( +
+
TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}
+
RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}
+
+ ); + }, + }, + ]; + + const filterOptions: FilterOption[] = [ + { + name: "Team ID", + label: "Team ID", + isSearchable: true, + searchFn: async (searchText: string) => { + if (!allTeams || allTeams.length === 0) return []; + + const filteredTeams = allTeams.filter( + (team) => + team.team_id.toLowerCase().includes(searchText.toLowerCase()) || + (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())), + ); + + return filteredTeams.map((team) => ({ + label: `${team.team_alias || team.team_id} (${team.team_id})`, + value: team.team_id, + })); + }, + }, + { + name: "Organization ID", + label: "Organization ID", + isSearchable: true, + searchFn: async (searchText: string) => { + if (!allOrganizations || allOrganizations.length === 0) return []; + + const filteredOrgs = allOrganizations.filter( + (org) => org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false, + ); + + return filteredOrgs + .filter((org) => org.organization_id !== null && org.organization_id !== undefined) + .map((org) => ({ + label: `${org.organization_id || "Unknown"} (${org.organization_id})`, + value: org.organization_id as string, + })); + }, + }, + { + name: "Key Alias", + label: "Key Alias", + isSearchable: true, + searchFn: async (searchText) => { + const filteredKeyAliases = allKeyAliases.filter((key) => { + return key.toLowerCase().includes(searchText.toLowerCase()); + }); + + return filteredKeyAliases.map((key) => { + return { + label: key, + value: key, + }; + }); + }, + }, + { + name: "User ID", + label: "User ID", + isSearchable: false, + }, + { + name: "Key Hash", + label: "Key Hash", + isSearchable: false, + }, + ]; + + console.log(`keys: ${JSON.stringify(keys)}`); + + const table = useReactTable({ + data: filteredKeys, + columns: columns.filter((col) => col.id !== "expander"), + state: { + sorting, + }, + onSortingChange: (updaterOrValue) => { + const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue; + console.log(`newSorting: ${JSON.stringify(newSorting)}`); + setSorting(newSorting); + if (newSorting && newSorting.length > 0) { + const sortState = newSorting[0]; + const sortBy = sortState.id; + const sortOrder = sortState.desc ? "desc" : "asc"; + console.log(`sortBy: ${sortBy}, sortOrder: ${sortOrder}`); + handleFilterChange({ + ...filters, + "Sort By": sortBy, + "Sort Order": sortOrder, + }); + onSortChange?.(sortBy, sortOrder); + } + }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + enableSorting: true, + manualSorting: false, + }); + + // Update local sorting state when currentSort prop changes + React.useEffect(() => { + if (currentSort) { + setSorting([ + { + id: currentSort.sortBy, + desc: currentSort.sortOrder === "desc", + }, + ]); + } + }, [currentSort]); + + return ( +
+ {selectedKeyId ? ( + setSelectedKeyId(null)} + keyData={filteredKeys.find((k) => k.token === selectedKeyId)} + onKeyDataUpdate={(updatedKeyData) => { + setKeys((keys) => + keys.map((key) => { + if (key.token === updatedKeyData.token) { + return updateExistingKeys(key, updatedKeyData); + } + return key; + }), + ); + if (refresh) refresh(); // Minimal fix: refresh the full key list after an update + }} + onDelete={() => { + setKeys((keys) => keys.filter((key) => key.token !== selectedKeyId)); + if (refresh) refresh(); // Minimal fix: refresh the full key list after a delete + }} + accessToken={accessToken} + userID={userID} + userRole={userRole} + teams={allTeams} + premiumUser={premiumUser} + setAccessToken={setAccessToken} + /> + ) : ( +
+
+ +
+ +
+ + Showing{" "} + {isLoading + ? "..." + : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`}{" "} + of {isLoading ? "..." : pagination.totalCount} results + + +
+ + Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages} + + + + + +
+
+
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + +
+
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
+ {header.id !== "actions" && ( +
+ {header.column.getIsSorted() ? ( + { + asc: , + desc: , + }[header.column.getIsSorted() as string] + ) : ( + + )} +
+ )} +
+
+ ))} +
+ ))} +
+ + {isLoading ? ( + + +
+

🚅 Loading keys...

+
+
+
+ ) : filteredKeys.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + 3 ? "px-0" : ""}`} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + +
+

No keys found

+
+
+
+ )} +
+
+
+
+
+
+ )} +
+ ); +}; + +export default VirtualKeysTable; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts new file mode 100644 index 0000000000..ac174a554e --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts @@ -0,0 +1,189 @@ +import { useCallback, useEffect, useState, useRef } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { debounce } from "lodash"; +import { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; +import { keyListCall, Organization } from "@/components/networking"; +import { defaultPageSize } from "@/components/constants"; +import { fetchAllKeyAliases, fetchAllOrganizations, fetchAllTeams } from "@/components/key_team_helpers/filter_helpers"; + +export interface FilterState { + "Team ID": string; + "Organization ID": string; + "Key Alias": string; + [key: string]: string; + "User ID": string; + "Sort By": string; + "Sort Order": string; +} + +export function useFilterLogic({ + keys, + teams, + organizations, + accessToken, +}: { + keys: KeyResponse[]; + teams: Team[] | null; + organizations: Organization[] | null; + accessToken: string | null; +}) { + const defaultFilters: FilterState = { + "Team ID": "", + "Organization ID": "", + "Key Alias": "", + "User ID": "", + "Sort By": "created_at", + "Sort Order": "desc", + }; + const [filters, setFilters] = useState(defaultFilters); + const [allTeams, setAllTeams] = useState(teams || []); + const [allOrganizations, setAllOrganizations] = useState(organizations || []); + const [filteredKeys, setFilteredKeys] = useState(keys); + const lastSearchTimestamp = useRef(0); + const debouncedSearch = useCallback( + debounce(async (filters: FilterState) => { + if (!accessToken) { + return; + } + + const currentTimestamp = Date.now(); + lastSearchTimestamp.current = currentTimestamp; + + try { + // Make the API call using userListCall with all filter parameters + const data = await keyListCall( + accessToken, + filters["Organization ID"] || null, + filters["Team ID"] || null, + filters["Key Alias"] || null, + filters["User ID"] || null, + filters["Key Hash"] || null, + 1, // Reset to first page when searching + defaultPageSize, + filters["Sort By"] || null, + filters["Sort Order"] || null, + ); + + // Only update state if this is the most recent search + if (currentTimestamp === lastSearchTimestamp.current) { + if (data) { + setFilteredKeys(data.keys); + console.log("called from debouncedSearch filters:", JSON.stringify(filters)); + console.log("called from debouncedSearch data:", JSON.stringify(data)); + } + } + } catch (error) { + console.error("Error searching users:", error); + } + }, 300), + [accessToken], + ); + // Apply filters to keys whenever keys or filters change + useEffect(() => { + if (!keys) { + setFilteredKeys([]); + return; + } + + let result = [...keys]; + + // Apply Team ID filter + if (filters["Team ID"]) { + result = result.filter((key) => key.team_id === filters["Team ID"]); + } + + // Apply Organization ID filter + if (filters["Organization ID"]) { + result = result.filter((key) => key.organization_id === filters["Organization ID"]); + } + + setFilteredKeys(result); + }, [keys, filters]); + + // Fetch all data for filters when component mounts + useEffect(() => { + const loadAllFilterData = async () => { + // Load all teams - no organization filter needed here + const teamsData = await fetchAllTeams(accessToken); + if (teamsData.length > 0) { + setAllTeams(teamsData); + } + + // Load all organizations + const orgsData = await fetchAllOrganizations(accessToken); + if (orgsData.length > 0) { + setAllOrganizations(orgsData); + } + }; + + if (accessToken) { + loadAllFilterData(); + } + }, [accessToken]); + + const queryAllKeysQuery = useQuery({ + queryKey: ["allKeys"], + queryFn: async () => { + if (!accessToken) throw new Error("Access token required"); + return await fetchAllKeyAliases(accessToken); + }, + enabled: !!accessToken, + }); + const allKeyAliases = queryAllKeysQuery.data || []; + + // Update teams and organizations when props change + useEffect(() => { + if (teams && teams.length > 0) { + setAllTeams((prevTeams) => { + // Only update if we don't already have a larger set of teams + return prevTeams.length < teams.length ? teams : prevTeams; + }); + } + }, [teams]); + + useEffect(() => { + if (organizations && organizations.length > 0) { + setAllOrganizations((prevOrgs) => { + // Only update if we don't already have a larger set of organizations + return prevOrgs.length < organizations.length ? organizations : prevOrgs; + }); + } + }, [organizations]); + + const handleFilterChange = (newFilters: Record) => { + // Update filters state + setFilters({ + "Team ID": newFilters["Team ID"] || "", + "Organization ID": newFilters["Organization ID"] || "", + "Key Alias": newFilters["Key Alias"] || "", + "User ID": newFilters["User ID"] || "", + "Sort By": newFilters["Sort By"] || "created_at", + "Sort Order": newFilters["Sort Order"] || "desc", + }); + + // Fetch keys based on new filters + const updatedFilters = { + ...filters, + ...newFilters, + }; + debouncedSearch(updatedFilters); + }; + + const handleFilterReset = () => { + // Reset filters state + setFilters(defaultFilters); + + // Reset selections + debouncedSearch(defaultFilters); + }; + + return { + filters, + filteredKeys, + allKeyAliases, + allTeams, + allOrganizations, + handleFilterChange, + handleFilterReset, + }; +} diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/hooks/useTeams.tsx b/ui/litellm-dashboard/src/app/(console)/virtual-keys/hooks/useTeams.tsx new file mode 100644 index 0000000000..d1208737ec --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/hooks/useTeams.tsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from "react"; +import { fetchTeams } from "@/app/(console)/virtual-keys/networking"; +import { Team } from "@/components/key_team_helpers/key_list"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +const useTeams = () => { + const [teams, setTeams] = useState([]); + const { accessToken, userId: userID, userRole } = useAuthorized(); + + useEffect(() => { + (async () => { + const fetched = await fetchTeams(accessToken, userID, userRole, null); + setTeams(fetched); + })(); + }, [accessToken, userID, userRole]); + + return teams; +}; + +export default useTeams; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/networking.ts b/ui/litellm-dashboard/src/app/(console)/virtual-keys/networking.ts new file mode 100644 index 0000000000..7fb09d61a5 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/networking.ts @@ -0,0 +1,17 @@ +import { Organization, teamListCall } from "@/components/networking"; + +export const fetchTeams = async ( + accessToken: string, + userID: string | null, + userRole: string | null, + currentOrg: Organization | null, +) => { + let givenTeams; + if (userRole != "Admin" && userRole != "Admin Viewer") { + givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || null, userID); + } else { + givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || null); + } + + return givenTeams; +}; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/page.tsx b/ui/litellm-dashboard/src/app/(console)/virtual-keys/page.tsx new file mode 100644 index 0000000000..9554905088 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/page.tsx @@ -0,0 +1,52 @@ +"use client"; + +import VirtualKeysTable from "@/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable"; +import { getCookie } from "@/utils/cookieUtils"; +import { jwtDecode } from "jwt-decode"; +import { useState } from "react"; +import useKeyList from "@/components/key_team_helpers/key_list"; +import { useRouter } from "next/navigation"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Col, Grid } from "@tremor/react"; +import CreateKey from "@/app/(console)/virtual-keys/components/CreateKey"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized" + +const VirtualKeysPage = () => { + const {accessToken, userRole} = useAuthorized(); + const [createClicked, setCreateClicked] = useState(false); + + const queryClient = new QueryClient(); + + const { keys, isLoading, error, pagination, refresh, setKeys } = useKeyList({ + selectedKeyAlias: null, + currentOrg: null, + accessToken: accessToken || "", + createClicked, + }); + + const addKey = (data: any) => { + setKeys((prevData) => (prevData ? [...prevData, data] : [data])); + setCreateClicked(() => !createClicked); + }; + + return ( + +
+ + + + {}} + organizations={null} + /> + + +
+
+ ); +}; + +export default VirtualKeysPage; diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 579e90e530..472b3f9a37 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -234,7 +234,6 @@ export default function CreateKeyPage() { teams={teams} keys={keys} setUserRole={setUserRole} - userEmail={userEmail} setUserEmail={setUserEmail} setTeams={setTeams} setKeys={setKeys} @@ -275,7 +274,6 @@ export default function CreateKeyPage() { teams={teams} keys={keys} setUserRole={setUserRole} - userEmail={userEmail} setUserEmail={setUserEmail} setTeams={setTeams} setKeys={setKeys} diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index e47691d3bb..d9326cb887 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -1,8 +1,4 @@ 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, @@ -16,29 +12,17 @@ import { AppstoreOutlined, DatabaseOutlined, FileTextOutlined, - LineOutlined, LineChartOutlined, SafetyOutlined, ExperimentOutlined, - ThunderboltOutlined, - LockOutlined, ToolOutlined, TagsOutlined, BgColorsOutlined, - MenuFoldOutlined, - MenuUnfoldOutlined, } from "@ant-design/icons"; -import { - old_admin_roles, - v2_admin_role_names, - all_admin_roles, - rolesAllowedToSeeUsage, - rolesWithWriteAccess, - internalUserRoles, - isAdminRole, -} from "../utils/roles"; +import { all_admin_roles, rolesWithWriteAccess, internalUserRoles, isAdminRole } from "../utils/roles"; import UsageIndicator from "./usage_indicator"; import { ConfigProvider } from "antd"; +import { useRouter } from "next/navigation"; const { Sider } = Layout; // Define the props type @@ -61,6 +45,7 @@ interface MenuItem { } const Sidebar: React.FC = ({ accessToken, setPage, userRole, defaultSelectedKey, collapsed = false }) => { + const router = useRouter(); // Note: If a menu item does not have a role, it is visible to all roles. const menuItems: MenuItem[] = [ { @@ -252,6 +237,19 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau return true; }); + const navigateToPage = (page: string) => { + if (page === "api-keys") { + // Go to clean route and DO NOT call setPage to avoid any parent sync that re-adds ?page=api-keys + router.replace("/virtual-keys"); + return; + } + const newSearchParams = new URLSearchParams(window.location.search); + newSearchParams.set("page", page); + // Use absolute root to replace everything after the domain + router.replace(`/?${newSearchParams.toString()}`); + setPage(page); + }; + return ( = ({ accessToken, setPage, userRole, defau key: child.key, icon: child.icon, label: child.label, - onClick: () => { - const newSearchParams = new URLSearchParams(window.location.search); - newSearchParams.set("page", child.page); - window.history.pushState(null, "", `?${newSearchParams.toString()}`); - setPage(child.page); - }, + onClick: () => navigateToPage(child.page), })), - 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, + onClick: !item.children ? () => navigateToPage(item.page) : undefined, }))} /> diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index c725466b98..20c1f5b3a3 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -131,6 +131,14 @@ export const fetchUserModels = async ( } }; + +/** + * ───────────────────────────────────────────────────────────────────────── + * @deprecated + * This component is being DEPRECATED in favor of src/app/(console)/virtual-keys/components/CreateKey.tsx + * Please contribute to the new refactor. + * ───────────────────────────────────────────────────────────────────────── + */ const CreateKey: React.FC = ({ userID, team, diff --git a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx index bad8bdb983..f3ba12d56d 100644 --- a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx @@ -135,6 +135,13 @@ interface CombinedLimits { [key: string]: CombinedLimit; // Index signature allowing string keys } +/** + * ───────────────────────────────────────────────────────────────────────── + * @deprecated + * This component is being DEPRECATED in favor of src/app/(console)/virtual-keys/components/VirtualKeysTable/ + * Please contribute to the new refactor. + * ───────────────────────────────────────────────────────────────────────── + */ const ViewKeyTable: React.FC = ({ userID, userRole, diff --git a/ui/litellm-dashboard/src/utils/roles.ts b/ui/litellm-dashboard/src/utils/roles.ts index aafeb010be..f542da105f 100644 --- a/ui/litellm-dashboard/src/utils/roles.ts +++ b/ui/litellm-dashboard/src/utils/roles.ts @@ -5,7 +5,7 @@ export const all_admin_roles = [...old_admin_roles, ...v2_admin_role_names]; export const internalUserRoles = ["Internal User", "Internal Viewer"]; export const rolesAllowedToSeeUsage = ["Admin", "Admin Viewer", "Internal User", "Internal Viewer"]; -export const rolesWithWriteAccess = ["Internal User", "Admin"]; +export const rolesWithWriteAccess = ["Internal User", "Admin", "proxy_admin"]; // Helper function to check if a role is in all_admin_roles export const isAdminRole = (role: string): boolean => { From 3f53d6b7ad32b7b55f9782f80e336bba55cd9a4c Mon Sep 17 00:00:00 2001 From: = Date: Mon, 6 Oct 2025 09:45:58 -0700 Subject: [PATCH 02/17] Moved KeyInfoView to refactored folder, added deprecation notice --- .../components/VirtualKeysTable/VirtualKeysTable.tsx | 2 +- .../src/components/templates/key_info_view.tsx | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx index 5c32a736ae..cdbd7cee43 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx @@ -15,10 +15,10 @@ import { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; import { Organization, userListCall } from "@/components/networking"; import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; import FilterComponent, { FilterOption } from "@/components/molecules/filter"; -import KeyInfoView from "@/components/templates/key_info_view"; import { useFilterLogic } from "@/components/key_team_helpers/filter_logic"; import useTeams from "@/app/(console)/virtual-keys/hooks/useTeams"; import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import KeyInfoView from "@/app/(console)/virtual-keys/components/KeyInfoView"; interface AllKeysTableProps { keys: KeyResponse[]; diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index a5ec177425..0080924fb6 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -47,6 +47,13 @@ interface KeyInfoViewProps { backButtonText?: string; } +/** + * ───────────────────────────────────────────────────────────────────────── + * @deprecated + * This component is being DEPRECATED in favor of src/app/(console)/virtual-keys/components/KeyInfoView.tsx + * Please contribute to the new refactor. + * ───────────────────────────────────────────────────────────────────────── + */ export default function KeyInfoView({ keyId, onClose, From 53ee3e2634f956c059bbf3b5449a061a46ba6666 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 6 Oct 2025 10:28:48 -0700 Subject: [PATCH 03/17] refactoredUI feature flag --- .../components/{Sidebar.tsx => Sidebar2.tsx} | 4 +- .../src/app/(console)/layout.tsx | 4 +- ui/litellm-dashboard/src/app/page.tsx | 24 +++++++--- .../src/components/leftnav.tsx | 46 +++++++++++-------- .../src/components/navbar.tsx | 21 ++++++++- .../src/hooks/useFeatureFlags.ts | 9 ++++ 6 files changed, 78 insertions(+), 30 deletions(-) rename ui/litellm-dashboard/src/app/(console)/components/{Sidebar.tsx => Sidebar2.tsx} (98%) create mode 100644 ui/litellm-dashboard/src/hooks/useFeatureFlags.ts diff --git a/ui/litellm-dashboard/src/app/(console)/components/Sidebar.tsx b/ui/litellm-dashboard/src/app/(console)/components/Sidebar2.tsx similarity index 98% rename from ui/litellm-dashboard/src/app/(console)/components/Sidebar.tsx rename to ui/litellm-dashboard/src/app/(console)/components/Sidebar2.tsx index ad60ba626a..b2477243a0 100644 --- a/ui/litellm-dashboard/src/app/(console)/components/Sidebar.tsx +++ b/ui/litellm-dashboard/src/app/(console)/components/Sidebar2.tsx @@ -44,7 +44,7 @@ interface MenuItem { icon?: React.ReactNode; } -const Sidebar: React.FC = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => { +const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => { const menuItems: MenuItem[] = [ { key: "1", page: "api-keys", label: "Virtual Keys", icon: }, { @@ -288,4 +288,4 @@ const Sidebar: React.FC = ({ accessToken, userRole, defaultSelecte ); }; -export default Sidebar; +export default Sidebar2; diff --git a/ui/litellm-dashboard/src/app/(console)/layout.tsx b/ui/litellm-dashboard/src/app/(console)/layout.tsx index cbfb5bdcb5..6ad84208f5 100644 --- a/ui/litellm-dashboard/src/app/(console)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(console)/layout.tsx @@ -3,7 +3,7 @@ import React from "react"; import Navbar from "@/components/navbar"; import { ThemeProvider } from "@/contexts/ThemeContext"; -import Sidebar from "@/app/(console)/components/Sidebar"; +import Sidebar2 from "@/app/(console)/components/Sidebar2"; import useAuthorized from "@/app/(console)/hooks/useAuthorized"; export default function Layout({ children }: { children: React.ReactNode }) { @@ -31,7 +31,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { />
- row.startsWith(name + "=")); @@ -116,6 +118,8 @@ export default function CreateKeyPage() { const [authLoading, setAuthLoading] = useState(true); const [userID, setUserID] = useState(null); + const { refactoredUIFlag, setRefactoredUIFlag } = useFeatureFlags(); + const invitation_id = searchParams.get("invitation_id"); // Get page from URL, default to 'api-keys' if not present @@ -254,16 +258,22 @@ export default function CreateKeyPage() { isPublicPage={false} sidebarCollapsed={sidebarCollapsed} onToggleSidebar={toggleSidebar} + refactoredUIFlag={refactoredUIFlag} + setRefactoredUIFlag={setRefactoredUIFlag} />
- + {refactoredUIFlag ? ( + + ) : ( + + )}
{page == "api-keys" ? ( diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index d9326cb887..34916cddb9 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -12,17 +12,29 @@ import { AppstoreOutlined, DatabaseOutlined, FileTextOutlined, + LineOutlined, LineChartOutlined, SafetyOutlined, ExperimentOutlined, + ThunderboltOutlined, + LockOutlined, ToolOutlined, TagsOutlined, BgColorsOutlined, + MenuFoldOutlined, + MenuUnfoldOutlined, } from "@ant-design/icons"; -import { all_admin_roles, rolesWithWriteAccess, internalUserRoles, isAdminRole } from "../utils/roles"; +import { + old_admin_roles, + v2_admin_role_names, + all_admin_roles, + rolesAllowedToSeeUsage, + rolesWithWriteAccess, + internalUserRoles, + isAdminRole, +} from "../utils/roles"; import UsageIndicator from "./usage_indicator"; import { ConfigProvider } from "antd"; -import { useRouter } from "next/navigation"; const { Sider } = Layout; // Define the props type @@ -45,7 +57,6 @@ interface MenuItem { } const Sidebar: React.FC = ({ accessToken, setPage, userRole, defaultSelectedKey, collapsed = false }) => { - const router = useRouter(); // Note: If a menu item does not have a role, it is visible to all roles. const menuItems: MenuItem[] = [ { @@ -237,19 +248,6 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau return true; }); - const navigateToPage = (page: string) => { - if (page === "api-keys") { - // Go to clean route and DO NOT call setPage to avoid any parent sync that re-adds ?page=api-keys - router.replace("/virtual-keys"); - return; - } - const newSearchParams = new URLSearchParams(window.location.search); - newSearchParams.set("page", page); - // Use absolute root to replace everything after the domain - router.replace(`/?${newSearchParams.toString()}`); - setPage(page); - }; - return ( = ({ accessToken, setPage, userRole, defau key: child.key, icon: child.icon, label: child.label, - onClick: () => navigateToPage(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 ? () => navigateToPage(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, }))} /> diff --git a/ui/litellm-dashboard/src/components/navbar.tsx b/ui/litellm-dashboard/src/components/navbar.tsx index 0764288057..e4a9c21c44 100644 --- a/ui/litellm-dashboard/src/components/navbar.tsx +++ b/ui/litellm-dashboard/src/components/navbar.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import React, { useState, useEffect } from "react"; import type { MenuProps } from "antd"; -import { Dropdown, Tooltip } from "antd"; +import { Dropdown, Tooltip, Switch } from "antd"; import { getProxyBaseUrl, Organization } from "@/components/networking"; import { defaultOrg } from "@/components/common_components/default_org"; import { @@ -19,6 +19,7 @@ import { clearTokenCookies } from "@/utils/cookieUtils"; import { fetchProxySettings } from "@/utils/proxyUtils"; import { useTheme } from "@/contexts/ThemeContext"; import { clearMCPAuthTokens } from "./mcp_tools/mcp_auth_storage"; +import useFeatureFlags from "@/hooks/useFeatureFlags"; interface NavbarProps { userID: string | null; @@ -31,6 +32,8 @@ interface NavbarProps { isPublicPage: boolean; sidebarCollapsed?: boolean; onToggleSidebar?: () => void; + refactoredUIFlag: boolean; + setRefactoredUIFlag: React.Dispatch>; } const Navbar: React.FC = ({ @@ -44,6 +47,8 @@ const Navbar: React.FC = ({ isPublicPage = false, sidebarCollapsed = false, onToggleSidebar, + refactoredUIFlag, + setRefactoredUIFlag, }) => { const baseUrl = getProxyBaseUrl(); const [logoutUrl, setLogoutUrl] = useState(""); @@ -79,6 +84,8 @@ const Navbar: React.FC = ({ const userItems: MenuProps["items"] = [ { key: "user-info", + // Prevent dropdown from closing when interacting with the toggle + onClick: (info) => info.domEvent?.stopPropagation(), label: (
@@ -115,6 +122,18 @@ const Navbar: React.FC = ({ {userEmail || "Unknown"}
+ + {/* NEW: Feature flag label + toggle below the email field */} +
+ Refactored UI + setRefactoredUIFlag(checked)} + aria-label="Toggle refactored UI feature flag" + /> +
), diff --git a/ui/litellm-dashboard/src/hooks/useFeatureFlags.ts b/ui/litellm-dashboard/src/hooks/useFeatureFlags.ts new file mode 100644 index 0000000000..b82f89e874 --- /dev/null +++ b/ui/litellm-dashboard/src/hooks/useFeatureFlags.ts @@ -0,0 +1,9 @@ +import { useState } from "react"; + +const useFeatureFlags = () => { + const [refactoredUIFlag, setRefactoredUIFlag] = useState(false); + + return { refactoredUIFlag, setRefactoredUIFlag }; +}; + +export default useFeatureFlags; From 715c4999a290a528a54ac3ccf6b01d08ae6811e9 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Mon, 6 Oct 2025 12:57:16 -0700 Subject: [PATCH 04/17] global feature flag state for app --- .../(console)/components/SidebarProvider.tsx | 34 +++++++++++++++++++ .../src/app/(console)/layout.tsx | 1 + ui/litellm-dashboard/src/app/layout.tsx | 5 ++- ui/litellm-dashboard/src/app/page.tsx | 21 +++--------- .../src/components/navbar.tsx | 7 ++-- .../src/hooks/useFeatureFlags.ts | 9 ----- .../src/hooks/useFeatureFlags.tsx | 26 ++++++++++++++ 7 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(console)/components/SidebarProvider.tsx delete mode 100644 ui/litellm-dashboard/src/hooks/useFeatureFlags.ts create mode 100644 ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx diff --git a/ui/litellm-dashboard/src/app/(console)/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/(console)/components/SidebarProvider.tsx new file mode 100644 index 0000000000..a5e150da86 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(console)/components/SidebarProvider.tsx @@ -0,0 +1,34 @@ +import Sidebar2 from "@/app/(console)/components/Sidebar2"; +import Sidebar from "@/components/leftnav"; +import React from "react"; +import useFeatureFlags from "@/hooks/useFeatureFlags"; +import useAuthorized from "@/app/(console)/hooks/useAuthorized"; + +interface SidebarProviderProps { + defaultSelectedKey: string; + sidebarCollapsed: boolean; + setPage: (page: string) => void; +} + +const SidebarProvider = ({defaultSelectedKey, sidebarCollapsed, setPage}: SidebarProviderProps) => { + + const {accessToken, userRole} = useAuthorized(); + const { refactoredUIFlag, setRefactoredUIFlag } = useFeatureFlags(); + + return ( + refactoredUIFlag ? ( + + ) : ( + + ) + ); + +} + +export default SidebarProvider; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/app/(console)/layout.tsx b/ui/litellm-dashboard/src/app/(console)/layout.tsx index 6ad84208f5..595eeedc59 100644 --- a/ui/litellm-dashboard/src/app/(console)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(console)/layout.tsx @@ -5,6 +5,7 @@ import Navbar from "@/components/navbar"; import { ThemeProvider } from "@/contexts/ThemeContext"; import Sidebar2 from "@/app/(console)/components/Sidebar2"; import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import useFeatureFlags, { FeatureFlagsProvider } from "@/hooks/useFeatureFlags"; export default function Layout({ children }: { children: React.ReactNode }) { const { accessToken, userRole } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/layout.tsx b/ui/litellm-dashboard/src/app/layout.tsx index 95c485fe2f..8a37d0fa1b 100644 --- a/ui/litellm-dashboard/src/app/layout.tsx +++ b/ui/litellm-dashboard/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; +import { FeatureFlagsProvider } from "@/hooks/useFeatureFlags"; const inter = Inter({ subsets: ["latin"] }); @@ -17,7 +18,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 2e49b422b4..eff997a3aa 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -39,8 +39,9 @@ import VectorStoreManagement from "@/components/vector_store_management"; import UIThemeSettings from "@/components/ui_theme_settings"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { cx } from "@/lib/cva.config"; -import useFeatureFlags from "@/hooks/useFeatureFlags"; +import useFeatureFlags, { FeatureFlagsProvider } from "@/hooks/useFeatureFlags"; import Sidebar2 from "@/app/(console)/components/Sidebar2"; +import SidebarProvider from "@/app/(console)/components/SidebarProvider"; function getCookie(name: string) { const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); @@ -118,8 +119,6 @@ export default function CreateKeyPage() { const [authLoading, setAuthLoading] = useState(true); const [userID, setUserID] = useState(null); - const { refactoredUIFlag, setRefactoredUIFlag } = useFeatureFlags(); - const invitation_id = searchParams.get("invitation_id"); // Get page from URL, default to 'api-keys' if not present @@ -234,6 +233,7 @@ export default function CreateKeyPage() {
- {refactoredUIFlag ? ( - - ) : ( - - )} +
{page == "api-keys" ? ( void; - refactoredUIFlag: boolean; - setRefactoredUIFlag: React.Dispatch>; } const Navbar: React.FC = ({ @@ -46,13 +44,12 @@ const Navbar: React.FC = ({ accessToken, isPublicPage = false, sidebarCollapsed = false, - onToggleSidebar, - refactoredUIFlag, - setRefactoredUIFlag, + onToggleSidebar }) => { const baseUrl = getProxyBaseUrl(); const [logoutUrl, setLogoutUrl] = useState(""); const { logoUrl } = useTheme(); + const { refactoredUIFlag, setRefactoredUIFlag } = useFeatureFlags(); // Simple logo URL: use custom logo if available, otherwise default const imageUrl = logoUrl || `${baseUrl}/get_image`; diff --git a/ui/litellm-dashboard/src/hooks/useFeatureFlags.ts b/ui/litellm-dashboard/src/hooks/useFeatureFlags.ts deleted file mode 100644 index b82f89e874..0000000000 --- a/ui/litellm-dashboard/src/hooks/useFeatureFlags.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useState } from "react"; - -const useFeatureFlags = () => { - const [refactoredUIFlag, setRefactoredUIFlag] = useState(false); - - return { refactoredUIFlag, setRefactoredUIFlag }; -}; - -export default useFeatureFlags; diff --git a/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx b/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx new file mode 100644 index 0000000000..d50fdf46a6 --- /dev/null +++ b/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx @@ -0,0 +1,26 @@ +'use client'; +import React, {createContext, useContext, useState} from 'react'; + +type Flags = { + refactoredUIFlag: boolean; + setRefactoredUIFlag: (v: boolean) => void; +}; + +const FeatureFlagsCtx = createContext(null); + +export const FeatureFlagsProvider = ({ children }: { children: React.ReactNode }) => { + const [refactoredUIFlag, setRefactoredUIFlag] = useState(false); + return ( + + {children} + +); +} + +const useFeatureFlags = () => { + const ctx = useContext(FeatureFlagsCtx); + if (!ctx) throw new Error('useFeatureFlags must be used within FeatureFlagsProvider'); + return ctx; +} + +export default useFeatureFlags; From f4d9e4869ad7c29647aed699de6e77e51584b4e8 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Mon, 6 Oct 2025 13:33:38 -0700 Subject: [PATCH 05/17] renamed (console), added use clients --- .../components/Sidebar2.tsx | 4 +++- .../components/SidebarProvider.tsx | 6 +++-- .../components/modals/CreateUserModal.tsx | 8 ++++--- .../hooks/useAuthorized.ts | 3 ++- .../app/{(console) => dashboard}/layout.tsx | 5 ++--- .../virtual-keys/components/CreateKey.tsx | 6 ++--- .../CreateKeyForm/KeyDetailsSection.tsx | 2 ++ .../CreateKeyForm/OptionalSettingsSection.tsx | 4 +++- .../CreateKeyForm/OwnershipSection.tsx | 4 +++- .../CreateKeyModal/CreateKeyModal.tsx | 22 ++++++++++--------- .../components/CreateKeyModal/hooks/index.ts | 0 .../hooks/useGuardrailsAndPrompts.ts | 4 ++-- .../hooks/useMcpAccessGroups.ts | 4 ++-- .../CreateKeyModal/hooks/useTeamModels.ts | 4 ++-- .../CreateKeyModal/hooks/useUserModels.ts | 4 ++-- .../CreateKeyModal/hooks/useUserSearch.ts | 4 ++-- .../components/CreateKeyModal/networking.ts | 2 +- .../components/CreateKeyModal/types.ts | 0 .../components/CreateKeyModal/utils.ts | 2 +- .../virtual-keys/components/KeyInfoView.tsx | 2 ++ .../virtual-keys/components/SaveKeyModal.tsx | 2 ++ .../VirtualKeysTable/VirtualKeysTable.tsx | 8 +++---- .../VirtualKeysTable/hooks/useFilterLogic.ts | 0 .../virtual-keys/hooks/useTeams.tsx | 4 ++-- .../virtual-keys/networking.ts | 0 .../virtual-keys/page.tsx | 9 +++----- ui/litellm-dashboard/src/app/page.tsx | 4 ++-- .../organisms/create_key_button.tsx | 2 +- .../components/templates/key_info_view.tsx | 2 +- .../components/templates/view_key_table.tsx | 2 +- 30 files changed, 69 insertions(+), 54 deletions(-) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/components/Sidebar2.tsx (99%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/components/SidebarProvider.tsx (86%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/components/modals/CreateUserModal.tsx (98%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/hooks/useAuthorized.ts (94%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/layout.tsx (86%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKey.tsx (94%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx (99%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx (99%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx (98%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx (91%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/hooks/index.ts (100%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts (83%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts (80%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts (89%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts (81%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts (89%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/networking.ts (95%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/types.ts (100%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/CreateKeyModal/utils.ts (98%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/KeyInfoView.tsx (99%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/SaveKeyModal.tsx (99%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx (99%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts (100%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/hooks/useTeams.tsx (79%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/networking.ts (100%) rename ui/litellm-dashboard/src/app/{(console) => dashboard}/virtual-keys/page.tsx (81%) diff --git a/ui/litellm-dashboard/src/app/(console)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/dashboard/components/Sidebar2.tsx similarity index 99% rename from ui/litellm-dashboard/src/app/(console)/components/Sidebar2.tsx rename to ui/litellm-dashboard/src/app/dashboard/components/Sidebar2.tsx index b2477243a0..9de9f2f136 100644 --- a/ui/litellm-dashboard/src/app/(console)/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/components/Sidebar2.tsx @@ -1,3 +1,5 @@ +"use client"; + import React from "react"; import Link from "next/link"; import { usePathname, useSearchParams } from "next/navigation"; @@ -216,7 +218,7 @@ const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelect }; const selectedMenuKey = - pathname === "/virtual-keys" + pathname === "/dashboard/virtual-keys" ? "1" : pageParam ? findMenuItemKey(pageParam) diff --git a/ui/litellm-dashboard/src/app/(console)/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/dashboard/components/SidebarProvider.tsx similarity index 86% rename from ui/litellm-dashboard/src/app/(console)/components/SidebarProvider.tsx rename to ui/litellm-dashboard/src/app/dashboard/components/SidebarProvider.tsx index a5e150da86..8380493798 100644 --- a/ui/litellm-dashboard/src/app/(console)/components/SidebarProvider.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/components/SidebarProvider.tsx @@ -1,8 +1,10 @@ -import Sidebar2 from "@/app/(console)/components/Sidebar2"; +"use client"; + +import Sidebar2 from "@/app/dashboard/components/Sidebar2"; import Sidebar from "@/components/leftnav"; import React from "react"; import useFeatureFlags from "@/hooks/useFeatureFlags"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; interface SidebarProviderProps { defaultSelectedKey: string; diff --git a/ui/litellm-dashboard/src/app/(console)/components/modals/CreateUserModal.tsx b/ui/litellm-dashboard/src/app/dashboard/components/modals/CreateUserModal.tsx similarity index 98% rename from ui/litellm-dashboard/src/app/(console)/components/modals/CreateUserModal.tsx rename to ui/litellm-dashboard/src/app/dashboard/components/modals/CreateUserModal.tsx index 4caaceb662..8b16074b25 100644 --- a/ui/litellm-dashboard/src/app/(console)/components/modals/CreateUserModal.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/components/modals/CreateUserModal.tsx @@ -1,3 +1,5 @@ +"use client"; + // TODO: refactor import React, { useState, useEffect } from "react"; import { Button, Modal, Form, Input, message, Select, InputNumber, Select as Select2 } from "antd"; @@ -27,9 +29,9 @@ import { } from "@/components/networking"; import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; import BulkCreateUsersButton from "@/components/bulk_create_users_button"; -import { fetchTeams } from "@/app/(console)/virtual-keys/networking"; -import useTeams from "@/app/(console)/virtual-keys/hooks/useTeams"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import { fetchTeams } from "@/app/dashboard/virtual-keys/networking"; +import useTeams from "@/app/dashboard/virtual-keys/hooks/useTeams"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; // Helper function to generate UUID compatible across all environments const generateUUID = (): string => { diff --git a/ui/litellm-dashboard/src/app/(console)/hooks/useAuthorized.ts b/ui/litellm-dashboard/src/app/dashboard/hooks/useAuthorized.ts similarity index 94% rename from ui/litellm-dashboard/src/app/(console)/hooks/useAuthorized.ts rename to ui/litellm-dashboard/src/app/dashboard/hooks/useAuthorized.ts index 45f5ee92ea..9d436d0566 100644 --- a/ui/litellm-dashboard/src/app/(console)/hooks/useAuthorized.ts +++ b/ui/litellm-dashboard/src/app/dashboard/hooks/useAuthorized.ts @@ -1,4 +1,5 @@ -import { Router } from "next/router"; +"use client"; + import { getCookie } from "@/utils/cookieUtils"; import { useRouter } from "next/navigation"; import { jwtDecode } from "jwt-decode"; diff --git a/ui/litellm-dashboard/src/app/(console)/layout.tsx b/ui/litellm-dashboard/src/app/dashboard/layout.tsx similarity index 86% rename from ui/litellm-dashboard/src/app/(console)/layout.tsx rename to ui/litellm-dashboard/src/app/dashboard/layout.tsx index 595eeedc59..851ecec665 100644 --- a/ui/litellm-dashboard/src/app/(console)/layout.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/layout.tsx @@ -3,9 +3,8 @@ import React from "react"; import Navbar from "@/components/navbar"; import { ThemeProvider } from "@/contexts/ThemeContext"; -import Sidebar2 from "@/app/(console)/components/Sidebar2"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; -import useFeatureFlags, { FeatureFlagsProvider } from "@/hooks/useFeatureFlags"; +import Sidebar2 from "@/app/dashboard/components/Sidebar2"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; export default function Layout({ children }: { children: React.ReactNode }) { const { accessToken, userRole } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKey.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKey.tsx similarity index 94% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKey.tsx rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKey.tsx index c8e0b1d8ce..15dd713917 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKey.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKey.tsx @@ -6,9 +6,9 @@ import { Modal, Form } from "antd"; import { getPossibleUserRoles, Organization } from "@/components/networking"; import { rolesWithWriteAccess } from "@/utils/roles"; import { Team } from "@/components/key_team_helpers/key_list"; -import CreateKeyModal from "@/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyModal"; -import CreateUserModal from "@/app/(console)/components/modals/CreateUserModal"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import CreateKeyModal from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyModal"; +import CreateUserModal from "@/app/dashboard/components/modals/CreateUserModal"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; interface CreateKeyProps { team: Team | null; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx similarity index 99% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx index 54e9aaefed..33a8ae06cf 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx @@ -1,3 +1,5 @@ +"use client"; + import { TextInput, Title } from "@tremor/react"; import { Form, FormInstance, Select, Tooltip } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx similarity index 99% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx index c879cdf595..65ebd2d571 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Accordion, AccordionBody, AccordionHeader, Text, Title } from "@tremor/react"; import { Form, FormInstance, Input, Select, Tooltip } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; @@ -15,7 +17,7 @@ import SchemaFormFields from "@/components/common_components/check_openapi_schem import { Team } from "@/components/key_team_helpers/key_list"; import React from "react"; import { DefaultOptionType } from "rc-select/lib/Select"; -import { ModelAliases } from "@/app/(console)/virtual-keys/components/CreateKeyModal/types"; +import { ModelAliases } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types"; export interface OptionalSettingsSectionProps { form: FormInstance; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx similarity index 98% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx index fdfb3893f6..00c2e9a1e2 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx @@ -1,10 +1,12 @@ +"use client"; + import { Button as Button2, Form, Radio, Select, Tooltip } from "antd"; import { Title } from "@tremor/react"; import { InfoCircleOutlined } from "@ant-design/icons"; import TeamDropdown from "@/components/common_components/team_dropdown"; import React from "react"; import { Team } from "@/components/key_team_helpers/key_list"; -import { UserOption } from "@/app/(console)/virtual-keys/components/CreateKeyModal/types"; +import { UserOption } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types"; interface OwnershipSectionProps { team: Team | null; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx similarity index 91% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx index 9f4d7528a8..2bee7f72b1 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx @@ -1,9 +1,11 @@ +"use client"; + import { Button as Button2, Form, FormInstance, Modal } from "antd"; import { Text } from "@tremor/react"; import { keyCreateCall, keyCreateServiceAccountCall } from "@/components/networking"; import React, { useEffect, useState } from "react"; import { Team } from "@/components/key_team_helpers/key_list"; -import SaveKeyModal from "@/app/(console)/virtual-keys/components/SaveKeyModal"; +import SaveKeyModal from "@/app/dashboard/virtual-keys/components/SaveKeyModal"; import NotificationsManager from "@/components/molecules/notifications_manager"; import { useGuardrailsAndPrompts, @@ -11,15 +13,15 @@ import { useTeamModels, useUserModels, useUserSearch, -} from "@/app/(console)/virtual-keys/components/CreateKeyModal/hooks"; -import OwnershipSection from "@/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection"; -import KeyDetailsSection from "@/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection"; -import OptionalSettingsSection from "@/app/(console)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection"; -import { ModelAliases } from "@/app/(console)/virtual-keys/components/CreateKeyModal/types"; -import { getPredefinedTags, prepareFormValues } from "@/app/(console)/virtual-keys/components/CreateKeyModal/utils"; -import { fetchTeams } from "@/app/(console)/virtual-keys/networking"; -import useTeams from "@/app/(console)/virtual-keys/hooks/useTeams"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +} from "@/app/dashboard/virtual-keys/components/CreateKeyModal/hooks"; +import OwnershipSection from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection"; +import KeyDetailsSection from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection"; +import OptionalSettingsSection from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection"; +import { ModelAliases } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types"; +import { getPredefinedTags, prepareFormValues } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/utils"; +import { fetchTeams } from "@/app/dashboard/virtual-keys/networking"; +import useTeams from "@/app/dashboard/virtual-keys/hooks/useTeams"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; export interface CreateKeyModalProps { isModalVisible: boolean; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/index.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/index.ts similarity index 100% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/index.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/index.ts diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts similarity index 83% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts index d092219f70..2445240e1e 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { fetchGuardrails, fetchPrompts } from "@/app/(console)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import { fetchGuardrails, fetchPrompts } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; export const useGuardrailsAndPrompts = () => { const [guardrails, setGuardrails] = useState([]); diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts similarity index 80% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts index 8313f6e8d6..5007040000 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { getMCPAccessGroups } from "@/app/(console)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import { getMCPAccessGroups } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; export const useMcpAccessGroups = () => { const [mcpAccessGroups, setMcpAccessGroups] = useState([]); diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts similarity index 89% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts index c28ef0ecdb..979337f5aa 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import type { Team } from "@/components/key_team_helpers/key_list"; -import { fetchTeamModels } from "@/app/(console)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import { fetchTeamModels } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; export const useTeamModels = (selectedTeam: Team | null) => { const { userId: userID, userRole, accessToken } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts similarity index 81% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts index ddba6879cc..badcaf93d9 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { getUserModelNames } from "@/app/(console)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import { getUserModelNames } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; export const useUserModels = () => { const { userId: userID, userRole, accessToken } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts similarity index 89% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts index 782beccf48..a3840bdc62 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { debounce } from "lodash"; -import { searchUserOptionsByEmail } from "@/app/(console)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import { searchUserOptionsByEmail } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; type User = { user_id: string; user_email: string; role?: string }; export interface UserOption { diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/networking.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/networking.ts similarity index 95% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/networking.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/networking.ts index 6cb4e2f7f0..2277800dad 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/networking.ts +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/networking.ts @@ -6,7 +6,7 @@ import { User, userFilterUICall, } from "@/components/networking"; -import { ModelAvailableResponse, UserOption } from "@/app/(console)/virtual-keys/components/CreateKeyModal/types"; +import { ModelAvailableResponse, UserOption } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types"; export const fetchGuardrails = async (accessToken: string) => { try { diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/types.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/types.ts similarity index 100% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/types.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/types.ts diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/utils.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/utils.ts similarity index 98% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/utils.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/utils.ts index 066770cf46..73cd433e78 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/CreateKeyModal/utils.ts +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/utils.ts @@ -1,5 +1,5 @@ import { mapDisplayToInternalNames } from "@/components/callback_info_helpers"; -import { ModelAliases } from "@/app/(console)/virtual-keys/components/CreateKeyModal/types"; +import { ModelAliases } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types"; export const getPredefinedTags = (data: any[] | null) => { let allTags = []; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/KeyInfoView.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/KeyInfoView.tsx similarity index 99% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/KeyInfoView.tsx rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/KeyInfoView.tsx index 1c7185f1dc..9f9a47fbe1 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/KeyInfoView.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/KeyInfoView.tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { useEffect, useState } from "react"; import { Card, diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/SaveKeyModal.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/SaveKeyModal.tsx similarity index 99% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/SaveKeyModal.tsx rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/SaveKeyModal.tsx index bd0d425c46..fed678aadc 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/SaveKeyModal.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/SaveKeyModal.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Button, Col, Grid, Text, Title } from "@tremor/react"; import { CopyToClipboard } from "react-copy-to-clipboard"; import { Modal } from "antd"; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx similarity index 99% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx index cdbd7cee43..b7727a299b 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx @@ -1,5 +1,5 @@ -// TODO: refactor "use client"; +// TODO: refactor import React, { useEffect, useState } from "react"; import { ColumnDef } from "@tanstack/react-table"; @@ -16,9 +16,9 @@ import { Organization, userListCall } from "@/components/networking"; import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; import FilterComponent, { FilterOption } from "@/components/molecules/filter"; import { useFilterLogic } from "@/components/key_team_helpers/filter_logic"; -import useTeams from "@/app/(console)/virtual-keys/hooks/useTeams"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; -import KeyInfoView from "@/app/(console)/virtual-keys/components/KeyInfoView"; +import useTeams from "@/app/dashboard/virtual-keys/hooks/useTeams"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +import KeyInfoView from "@/app/dashboard/virtual-keys/components/KeyInfoView"; interface AllKeysTableProps { keys: KeyResponse[]; diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts similarity index 100% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/hooks/useTeams.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/hooks/useTeams.tsx similarity index 79% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/hooks/useTeams.tsx rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/hooks/useTeams.tsx index d1208737ec..92e5896e76 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/hooks/useTeams.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/hooks/useTeams.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { fetchTeams } from "@/app/(console)/virtual-keys/networking"; +import { fetchTeams } from "@/app/dashboard/virtual-keys/networking"; import { Team } from "@/components/key_team_helpers/key_list"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; const useTeams = () => { const [teams, setTeams] = useState([]); diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/networking.ts b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/networking.ts similarity index 100% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/networking.ts rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/networking.ts diff --git a/ui/litellm-dashboard/src/app/(console)/virtual-keys/page.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/page.tsx similarity index 81% rename from ui/litellm-dashboard/src/app/(console)/virtual-keys/page.tsx rename to ui/litellm-dashboard/src/app/dashboard/virtual-keys/page.tsx index 9554905088..7dfc959867 100644 --- a/ui/litellm-dashboard/src/app/(console)/virtual-keys/page.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/page.tsx @@ -1,15 +1,12 @@ "use client"; -import VirtualKeysTable from "@/app/(console)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable"; -import { getCookie } from "@/utils/cookieUtils"; -import { jwtDecode } from "jwt-decode"; +import VirtualKeysTable from "@/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable"; import { useState } from "react"; import useKeyList from "@/components/key_team_helpers/key_list"; -import { useRouter } from "next/navigation"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Col, Grid } from "@tremor/react"; -import CreateKey from "@/app/(console)/virtual-keys/components/CreateKey"; -import useAuthorized from "@/app/(console)/hooks/useAuthorized" +import CreateKey from "@/app/dashboard/virtual-keys/components/CreateKey"; +import useAuthorized from "@/app/dashboard/hooks/useAuthorized" const VirtualKeysPage = () => { const {accessToken, userRole} = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index eff997a3aa..3815df002b 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -40,8 +40,8 @@ import UIThemeSettings from "@/components/ui_theme_settings"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { cx } from "@/lib/cva.config"; import useFeatureFlags, { FeatureFlagsProvider } from "@/hooks/useFeatureFlags"; -import Sidebar2 from "@/app/(console)/components/Sidebar2"; -import SidebarProvider from "@/app/(console)/components/SidebarProvider"; +import Sidebar2 from "@/app/dashboard/components/Sidebar2"; +import SidebarProvider from "@/app/dashboard/components/SidebarProvider"; function getCookie(name: string) { const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index 20c1f5b3a3..d4035e83f0 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -135,7 +135,7 @@ export const fetchUserModels = async ( /** * ───────────────────────────────────────────────────────────────────────── * @deprecated - * This component is being DEPRECATED in favor of src/app/(console)/virtual-keys/components/CreateKey.tsx + * This component is being DEPRECATED in favor of src/app/dashboard/virtual-keys/components/CreateKey.tsx * Please contribute to the new refactor. * ───────────────────────────────────────────────────────────────────────── */ diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index 0080924fb6..ba04e2644a 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -50,7 +50,7 @@ interface KeyInfoViewProps { /** * ───────────────────────────────────────────────────────────────────────── * @deprecated - * This component is being DEPRECATED in favor of src/app/(console)/virtual-keys/components/KeyInfoView.tsx + * This component is being DEPRECATED in favor of src/app/dashboard/virtual-keys/components/KeyInfoView.tsx * Please contribute to the new refactor. * ───────────────────────────────────────────────────────────────────────── */ diff --git a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx index f3ba12d56d..3fad7c0a35 100644 --- a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx @@ -138,7 +138,7 @@ interface CombinedLimits { /** * ───────────────────────────────────────────────────────────────────────── * @deprecated - * This component is being DEPRECATED in favor of src/app/(console)/virtual-keys/components/VirtualKeysTable/ + * This component is being DEPRECATED in favor of src/app/dashboard/virtual-keys/components/VirtualKeysTable/ * Please contribute to the new refactor. * ───────────────────────────────────────────────────────────────────────── */ From cee408c0c06403b68837cd8cfc34d36c6a3ebc69 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Mon, 6 Oct 2025 14:03:53 -0700 Subject: [PATCH 06/17] diagnosing next build issue --- .../src/app/dashboard/hooks/useAuthorized.ts | 39 +++++++++++++------ ui/litellm-dashboard/src/app/layout.tsx | 9 +++-- ui/litellm-dashboard/src/utils/cookieUtils.ts | 1 + 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/ui/litellm-dashboard/src/app/dashboard/hooks/useAuthorized.ts b/ui/litellm-dashboard/src/app/dashboard/hooks/useAuthorized.ts index 9d436d0566..74c0476f19 100644 --- a/ui/litellm-dashboard/src/app/dashboard/hooks/useAuthorized.ts +++ b/ui/litellm-dashboard/src/app/dashboard/hooks/useAuthorized.ts @@ -1,24 +1,41 @@ "use client"; -import { getCookie } from "@/utils/cookieUtils"; +import { useEffect, useMemo } from "react"; +import { getCookie, clearTokenCookies } from "@/utils/cookieUtils"; import { useRouter } from "next/navigation"; import { jwtDecode } from "jwt-decode"; const useAuthorized = () => { - const token = getCookie("token"); const router = useRouter(); - if (!token) { - router.replace("/sso/key/generate"); - } + const token = typeof document !== "undefined" ? getCookie("token") : null; - const decoded = jwtDecode(token as string) as { [key: string]: any }; - const accessToken = decoded.key; - const userId = decoded.user_id; - const userRole = decoded.user_role; - const premiumUser = decoded.premium_user; + // Redirect after mount if missing/invalid token + useEffect(() => { + if (!token) { + router.replace("/sso/key/generate"); + } + }, [token, router]); - return { accessToken, userId, userRole, premiumUser }; + // Decode safely + const decoded = useMemo(() => { + if (!token) return null; + try { + return jwtDecode(token) as Record; + } catch { + // Bad token in cookie — clear and bounce + clearTokenCookies(); + router.replace("/sso/key/generate"); + return null; + } + }, [token, router]); + + return { + accessToken: decoded?.key ?? null, + userId: decoded?.user_id ?? null, + userRole: decoded?.user_role ?? null, + premiumUser: decoded?.premium_user ?? null, + }; }; export default useAuthorized; diff --git a/ui/litellm-dashboard/src/app/layout.tsx b/ui/litellm-dashboard/src/app/layout.tsx index 8a37d0fa1b..9b6c027a56 100644 --- a/ui/litellm-dashboard/src/app/layout.tsx +++ b/ui/litellm-dashboard/src/app/layout.tsx @@ -18,9 +18,12 @@ export default function RootLayout({ }>) { return ( - - {children} - + + + + {children} + + ); } diff --git a/ui/litellm-dashboard/src/utils/cookieUtils.ts b/ui/litellm-dashboard/src/utils/cookieUtils.ts index 2cc9ef61eb..e477a9f77e 100644 --- a/ui/litellm-dashboard/src/utils/cookieUtils.ts +++ b/ui/litellm-dashboard/src/utils/cookieUtils.ts @@ -37,6 +37,7 @@ export function clearTokenCookies() { * @returns The cookie value or null if not found */ export function getCookie(name: string) { + if (typeof document === "undefined") return null; const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); return cookieValue ? cookieValue.split("=")[1] : null; } From 00f61b214c04c5c78380f74898cdd50cbf2ccdd4 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Mon, 6 Oct 2025 14:17:02 -0700 Subject: [PATCH 07/17] more window guards --- .../VirtualKeysTable/VirtualKeysTable.tsx | 23 ++++++++++--------- ui/litellm-dashboard/src/utils/cookieUtils.ts | 4 ++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx index b7727a299b..cbf10f9771 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx @@ -105,18 +105,19 @@ const VirtualKeysTable = ({ // Add a useEffect to call refresh when a key is created useEffect(() => { - if (refresh) { - const handleStorageChange = () => { - refresh(); - }; - - // Listen for storage events that might indicate a key was created - window.addEventListener("storage", handleStorageChange); - - return () => { - window.removeEventListener("storage", handleStorageChange); - }; + if (!refresh || typeof window === "undefined") { + return; } + const handleStorageChange = () => { + refresh(); + }; + + // Listen for storage events that might indicate a key was created + window.addEventListener("storage", handleStorageChange); + + return () => { + window.removeEventListener("storage", handleStorageChange); + }; }, [refresh]); const columns: ColumnDef[] = [ diff --git a/ui/litellm-dashboard/src/utils/cookieUtils.ts b/ui/litellm-dashboard/src/utils/cookieUtils.ts index e477a9f77e..23682fd6e8 100644 --- a/ui/litellm-dashboard/src/utils/cookieUtils.ts +++ b/ui/litellm-dashboard/src/utils/cookieUtils.ts @@ -6,6 +6,10 @@ * Clears the token cookie from both root and /ui paths */ export function clearTokenCookies() { + if (typeof window === "undefined" || typeof document === "undefined") { + return; + } + // Get the current domain const domain = window.location.hostname; From b1f3553ae4b7b0140540eb910c540d6e77f1a863 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Mon, 6 Oct 2025 14:28:12 -0700 Subject: [PATCH 08/17] Update networking.tsx --- .../src/components/networking.tsx | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 5f27c277d4..2ce673ee54 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -34,18 +34,34 @@ if (isLocal != true) { console.log = function () {}; } +const getWindowLocation = () => { + if (typeof window === "undefined") { + return null; + } + return window.location; +}; + const updateProxyBaseUrl = (serverRootPath: string, receivedProxyBaseUrl: string | null = null) => { /** * Special function for updating the proxy base url. Should only be called by getUiConfig. */ - const defaultProxyBaseUrl = isLocal ? "http://localhost:4000" : window.location.origin; - let initialProxyBaseUrl = receivedProxyBaseUrl || defaultProxyBaseUrl; + const browserLocation = getWindowLocation(); + const resolvedDefaultProxyBaseUrl = isLocal ? "http://localhost:4000" : browserLocation?.origin ?? null; + let initialProxyBaseUrl = receivedProxyBaseUrl || resolvedDefaultProxyBaseUrl; console.log("proxyBaseUrl:", proxyBaseUrl); console.log("serverRootPath:", serverRootPath); + + if (!initialProxyBaseUrl) { + proxyBaseUrl = proxyBaseUrl ?? null; + console.log("Updated proxyBaseUrl:", proxyBaseUrl); + return; + } + if (serverRootPath.length > 0 && !initialProxyBaseUrl.endsWith(serverRootPath) && serverRootPath != "/") { initialProxyBaseUrl += serverRootPath; - proxyBaseUrl = initialProxyBaseUrl; } + + proxyBaseUrl = initialProxyBaseUrl; console.log("Updated proxyBaseUrl:", proxyBaseUrl); }; @@ -54,7 +70,11 @@ const updateServerRootPath = (receivedServerRootPath: string) => { }; export const getProxyBaseUrl = (): string => { - return proxyBaseUrl ? proxyBaseUrl : window.location.origin; + if (proxyBaseUrl) { + return proxyBaseUrl; + } + const browserLocation = getWindowLocation(); + return browserLocation?.origin ?? ""; }; const HTTP_REQUEST = { @@ -159,7 +179,10 @@ const handleError = async (errorData: string) => { NotificationsManager.info("UI Session Expired. Logging out."); lastErrorTime = currentTime; clearTokenCookies(); - window.location.href = window.location.pathname; + const browserLocation = getWindowLocation(); + if (browserLocation) { + window.location.href = browserLocation.pathname; + } } lastErrorTime = currentTime; } else { From 6529ed4d620210efccc51912e413cb1ac4333041 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Mon, 6 Oct 2025 19:21:34 -0700 Subject: [PATCH 09/17] hidden dashboard routing, dev and build env files --- ui/litellm-dashboard/.env.development | 2 + ui/litellm-dashboard/.env.production | 2 + .../components/Sidebar2.tsx | 33 ++++++-- .../components/SidebarProvider.tsx | 32 +++++++ .../components/modals/CreateUserModal.tsx | 6 +- .../hooks/useAuthorized.ts | 0 .../app/{dashboard => (dashboard)}/layout.tsx | 4 +- .../virtual-keys/components/CreateKey.tsx | 6 +- .../CreateKeyForm/KeyDetailsSection.tsx | 0 .../CreateKeyForm/OptionalSettingsSection.tsx | 2 +- .../CreateKeyForm/OwnershipSection.tsx | 2 +- .../CreateKeyModal/CreateKeyModal.tsx | 20 ++--- .../components/CreateKeyModal/hooks/index.ts | 0 .../hooks/useGuardrailsAndPrompts.ts | 4 +- .../hooks/useMcpAccessGroups.ts | 4 +- .../CreateKeyModal/hooks/useTeamModels.ts | 4 +- .../CreateKeyModal/hooks/useUserModels.ts | 4 +- .../CreateKeyModal/hooks/useUserSearch.ts | 4 +- .../components/CreateKeyModal/networking.ts | 2 +- .../components/CreateKeyModal/types.ts | 0 .../components/CreateKeyModal/utils.ts | 2 +- .../virtual-keys/components/KeyInfoView.tsx | 0 .../virtual-keys/components/SaveKeyModal.tsx | 0 .../VirtualKeysTable/VirtualKeysTable.tsx | 6 +- .../VirtualKeysTable/hooks/useFilterLogic.ts | 0 .../virtual-keys/hooks/useTeams.tsx | 4 +- .../virtual-keys/networking.ts | 0 .../virtual-keys/page.tsx | 8 +- .../dashboard/components/SidebarProvider.tsx | 36 -------- ui/litellm-dashboard/src/app/page.tsx | 8 +- .../src/components/leftnav.tsx | 37 +++++---- .../organisms/create_key_button.tsx | 3 +- .../components/templates/key_info_view.tsx | 2 +- .../components/templates/view_key_table.tsx | 2 +- .../src/hooks/useFeatureFlags.tsx | 83 ++++++++++++++++--- 35 files changed, 208 insertions(+), 114 deletions(-) create mode 100644 ui/litellm-dashboard/.env.development create mode 100644 ui/litellm-dashboard/.env.production rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/components/Sidebar2.tsx (87%) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/components/modals/CreateUserModal.tsx (98%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/hooks/useAuthorized.ts (100%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/layout.tsx (91%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKey.tsx (92%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx (100%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx (99%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx (97%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx (89%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/hooks/index.ts (100%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts (76%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts (73%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts (85%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts (75%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts (84%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/networking.ts (95%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/types.ts (100%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/CreateKeyModal/utils.ts (98%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/KeyInfoView.tsx (100%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/SaveKeyModal.tsx (100%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx (99%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts (100%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/hooks/useTeams.tsx (77%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/networking.ts (100%) rename ui/litellm-dashboard/src/app/{dashboard => (dashboard)}/virtual-keys/page.tsx (81%) delete mode 100644 ui/litellm-dashboard/src/app/dashboard/components/SidebarProvider.tsx diff --git a/ui/litellm-dashboard/.env.development b/ui/litellm-dashboard/.env.development new file mode 100644 index 0000000000..b408125408 --- /dev/null +++ b/ui/litellm-dashboard/.env.development @@ -0,0 +1,2 @@ +NODE_ENV=development +NEXT_PUBLIC_BASE_URL="" \ No newline at end of file diff --git a/ui/litellm-dashboard/.env.production b/ui/litellm-dashboard/.env.production new file mode 100644 index 0000000000..1df897e7d2 --- /dev/null +++ b/ui/litellm-dashboard/.env.production @@ -0,0 +1,2 @@ +NODE_ENV=production +NEXT_PUBLIC_BASE_URL="ui/" \ No newline at end of file diff --git a/ui/litellm-dashboard/src/app/dashboard/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx similarity index 87% rename from ui/litellm-dashboard/src/app/dashboard/components/Sidebar2.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx index 9de9f2f136..209cf2e67f 100644 --- a/ui/litellm-dashboard/src/app/dashboard/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx @@ -46,6 +46,21 @@ interface MenuItem { icon?: React.ReactNode; } +/** ---------- Base URL helpers ---------- */ +/** Normalizes NEXT_PUBLIC_BASE_URL into either "" or "/something" (no trailing slash). */ +const getBasePath = () => { + const raw = process.env.NEXT_PUBLIC_BASE_URL ?? ""; + const trimmed = raw.replace(/^\/+|\/+$/g, ""); // strip leading/trailing slashes + return trimmed ? `/${trimmed}` : ""; +}; + +/** Joins base path with a relative path like "virtual-keys" or "/virtual-keys" -> "/base/virtual-keys" */ +const withBase = (relativePath: string) => { + const base = getBasePath(); // "" or "/ui" (no trailing slash) + const rel = relativePath.replace(/^\/+/, ""); // drop any leading slash + return `${base}/${rel}`.replace(/\/{2,}/g, "/"); // collapse accidental doubles +}; + const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => { const menuItems: MenuItem[] = [ { key: "1", page: "api-keys", label: "Virtual Keys", icon: }, @@ -217,8 +232,11 @@ const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelect return "1"; }; + // Match Virtual Keys path with base prefix (e.g., "/virtual-keys" or "/ui/virtual-keys") + const virtualKeysPath = withBase("virtual-keys"); + const selectedMenuKey = - pathname === "/dashboard/virtual-keys" + pathname === virtualKeysPath ? "1" : pageParam ? findMenuItemKey(pageParam) @@ -226,16 +244,19 @@ const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelect ? findMenuItemKey(defaultSelectedKey) : "1"; - // Root-only routing helper: always replace everything after the domain - const rootWithPage = (p: string) => ({ pathname: "/", query: { page: p } }); + // Root-only routing helper: always replace everything after the domain, honoring base path + const rootWithPage = (p: string) => ({ + pathname: getBasePath() || "/", + query: { page: p }, + }); // Convert to AntD Menu items: - // - "Virtual Keys" still routes to /virtual-keys - // - All other items (and children) route to "/?page=" + // - "Virtual Keys" routes to "//virtual-keys" + // - All other items (and children) route to "//?page=" const antdItems = filteredMenuItems.map((item) => { const isVirtualKeys = item.key === "1"; const label = isVirtualKeys ? ( - Virtual Keys + Virtual Keys ) : ( {item.label} ); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx new file mode 100644 index 0000000000..822a30e640 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx @@ -0,0 +1,32 @@ +"use client"; + +import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; +import Sidebar from "@/components/leftnav"; +import React from "react"; +import useFeatureFlags from "@/hooks/useFeatureFlags"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +interface SidebarProviderProps { + defaultSelectedKey: string; + sidebarCollapsed: boolean; + setPage: (page: string) => void; +} + +const SidebarProvider = ({ defaultSelectedKey, sidebarCollapsed, setPage }: SidebarProviderProps) => { + const { accessToken, userRole } = useAuthorized(); + const { refactoredUIFlag, setRefactoredUIFlag } = useFeatureFlags(); + + return refactoredUIFlag ? ( + + ) : ( + + ); +}; + +export default SidebarProvider; diff --git a/ui/litellm-dashboard/src/app/dashboard/components/modals/CreateUserModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx similarity index 98% rename from ui/litellm-dashboard/src/app/dashboard/components/modals/CreateUserModal.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx index 8b16074b25..fce7f69192 100644 --- a/ui/litellm-dashboard/src/app/dashboard/components/modals/CreateUserModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx @@ -29,9 +29,9 @@ import { } from "@/components/networking"; import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; import BulkCreateUsersButton from "@/components/bulk_create_users_button"; -import { fetchTeams } from "@/app/dashboard/virtual-keys/networking"; -import useTeams from "@/app/dashboard/virtual-keys/hooks/useTeams"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking"; +import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; // Helper function to generate UUID compatible across all environments const generateUUID = (): string => { diff --git a/ui/litellm-dashboard/src/app/dashboard/hooks/useAuthorized.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts similarity index 100% rename from ui/litellm-dashboard/src/app/dashboard/hooks/useAuthorized.ts rename to ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts diff --git a/ui/litellm-dashboard/src/app/dashboard/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx similarity index 91% rename from ui/litellm-dashboard/src/app/dashboard/layout.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/layout.tsx index 851ecec665..ad8a82703f 100644 --- a/ui/litellm-dashboard/src/app/dashboard/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx @@ -3,8 +3,8 @@ import React from "react"; import Navbar from "@/components/navbar"; import { ThemeProvider } from "@/contexts/ThemeContext"; -import Sidebar2 from "@/app/dashboard/components/Sidebar2"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; export default function Layout({ children }: { children: React.ReactNode }) { const { accessToken, userRole } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKey.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx similarity index 92% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKey.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx index 15dd713917..82be28e495 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKey.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx @@ -6,9 +6,9 @@ import { Modal, Form } from "antd"; import { getPossibleUserRoles, Organization } from "@/components/networking"; import { rolesWithWriteAccess } from "@/utils/roles"; import { Team } from "@/components/key_team_helpers/key_list"; -import CreateKeyModal from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyModal"; -import CreateUserModal from "@/app/dashboard/components/modals/CreateUserModal"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +import CreateKeyModal from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal"; +import CreateUserModal from "@/app/(dashboard)/components/modals/CreateUserModal"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; interface CreateKeyProps { team: Team | null; diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx similarity index 100% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx similarity index 99% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx index 65ebd2d571..b88f891944 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx @@ -17,7 +17,7 @@ import SchemaFormFields from "@/components/common_components/check_openapi_schem import { Team } from "@/components/key_team_helpers/key_list"; import React from "react"; import { DefaultOptionType } from "rc-select/lib/Select"; -import { ModelAliases } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types"; +import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; export interface OptionalSettingsSectionProps { form: FormInstance; diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx similarity index 97% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx index 00c2e9a1e2..f08b4ea367 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx @@ -6,7 +6,7 @@ import { InfoCircleOutlined } from "@ant-design/icons"; import TeamDropdown from "@/components/common_components/team_dropdown"; import React from "react"; import { Team } from "@/components/key_team_helpers/key_list"; -import { UserOption } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types"; +import { UserOption } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; interface OwnershipSectionProps { team: Team | null; diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx similarity index 89% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx index 2bee7f72b1..cbe3db4e8f 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx @@ -5,7 +5,7 @@ import { Text } from "@tremor/react"; import { keyCreateCall, keyCreateServiceAccountCall } from "@/components/networking"; import React, { useEffect, useState } from "react"; import { Team } from "@/components/key_team_helpers/key_list"; -import SaveKeyModal from "@/app/dashboard/virtual-keys/components/SaveKeyModal"; +import SaveKeyModal from "@/app/(dashboard)/virtual-keys/components/SaveKeyModal"; import NotificationsManager from "@/components/molecules/notifications_manager"; import { useGuardrailsAndPrompts, @@ -13,15 +13,15 @@ import { useTeamModels, useUserModels, useUserSearch, -} from "@/app/dashboard/virtual-keys/components/CreateKeyModal/hooks"; -import OwnershipSection from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection"; -import KeyDetailsSection from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection"; -import OptionalSettingsSection from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection"; -import { ModelAliases } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types"; -import { getPredefinedTags, prepareFormValues } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/utils"; -import { fetchTeams } from "@/app/dashboard/virtual-keys/networking"; -import useTeams from "@/app/dashboard/virtual-keys/hooks/useTeams"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +} from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks"; +import OwnershipSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection"; +import KeyDetailsSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection"; +import OptionalSettingsSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection"; +import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; +import { getPredefinedTags, prepareFormValues } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils"; +import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking"; +import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; export interface CreateKeyModalProps { isModalVisible: boolean; diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/index.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/index.ts similarity index 100% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/index.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/index.ts diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts similarity index 76% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts index 2445240e1e..f4718e8ba0 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { fetchGuardrails, fetchPrompts } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +import { fetchGuardrails, fetchPrompts } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; export const useGuardrailsAndPrompts = () => { const [guardrails, setGuardrails] = useState([]); diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts similarity index 73% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts index 5007040000..998b392f77 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { getMCPAccessGroups } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +import { getMCPAccessGroups } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; export const useMcpAccessGroups = () => { const [mcpAccessGroups, setMcpAccessGroups] = useState([]); diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts similarity index 85% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts index 979337f5aa..dd99de0615 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import type { Team } from "@/components/key_team_helpers/key_list"; -import { fetchTeamModels } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +import { fetchTeamModels } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; export const useTeamModels = (selectedTeam: Team | null) => { const { userId: userID, userRole, accessToken } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts similarity index 75% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts index badcaf93d9..66b5520f06 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { getUserModelNames } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +import { getUserModelNames } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; export const useUserModels = () => { const { userId: userID, userRole, accessToken } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts similarity index 84% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts index a3840bdc62..d266a51667 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { debounce } from "lodash"; -import { searchUserOptionsByEmail } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +import { searchUserOptionsByEmail } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; type User = { user_id: string; user_email: string; role?: string }; export interface UserOption { diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/networking.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts similarity index 95% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/networking.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts index 2277800dad..377f6d3113 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/networking.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts @@ -6,7 +6,7 @@ import { User, userFilterUICall, } from "@/components/networking"; -import { ModelAvailableResponse, UserOption } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types"; +import { ModelAvailableResponse, UserOption } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; export const fetchGuardrails = async (accessToken: string) => { try { diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/types.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/types.ts similarity index 100% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/types.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/types.ts diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/utils.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts similarity index 98% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/utils.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts index 73cd433e78..1fb40b8789 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/CreateKeyModal/utils.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts @@ -1,5 +1,5 @@ import { mapDisplayToInternalNames } from "@/components/callback_info_helpers"; -import { ModelAliases } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types"; +import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; export const getPredefinedTags = (data: any[] | null) => { let allTags = []; diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/KeyInfoView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx similarity index 100% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/KeyInfoView.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/SaveKeyModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/SaveKeyModal.tsx similarity index 100% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/SaveKeyModal.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/SaveKeyModal.tsx diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx similarity index 99% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx index cbf10f9771..015d8c43f9 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx @@ -16,9 +16,9 @@ import { Organization, userListCall } from "@/components/networking"; import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; import FilterComponent, { FilterOption } from "@/components/molecules/filter"; import { useFilterLogic } from "@/components/key_team_helpers/filter_logic"; -import useTeams from "@/app/dashboard/virtual-keys/hooks/useTeams"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; -import KeyInfoView from "@/app/dashboard/virtual-keys/components/KeyInfoView"; +import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import KeyInfoView from "@/app/(dashboard)/virtual-keys/components/KeyInfoView"; interface AllKeysTableProps { keys: KeyResponse[]; diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts similarity index 100% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/hooks/useTeams.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx similarity index 77% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/hooks/useTeams.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx index 92e5896e76..5f04f8a28a 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/hooks/useTeams.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { fetchTeams } from "@/app/dashboard/virtual-keys/networking"; +import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking"; import { Team } from "@/components/key_team_helpers/key_list"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; const useTeams = () => { const [teams, setTeams] = useState([]); diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/networking.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/networking.ts similarity index 100% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/networking.ts rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/networking.ts diff --git a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx similarity index 81% rename from ui/litellm-dashboard/src/app/dashboard/virtual-keys/page.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx index 7dfc959867..611d92f1a3 100644 --- a/ui/litellm-dashboard/src/app/dashboard/virtual-keys/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx @@ -1,15 +1,15 @@ "use client"; -import VirtualKeysTable from "@/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable"; +import VirtualKeysTable from "@/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable"; import { useState } from "react"; import useKeyList from "@/components/key_team_helpers/key_list"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Col, Grid } from "@tremor/react"; -import CreateKey from "@/app/dashboard/virtual-keys/components/CreateKey"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized" +import CreateKey from "@/app/(dashboard)/virtual-keys/components/CreateKey"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; const VirtualKeysPage = () => { - const {accessToken, userRole} = useAuthorized(); + const { accessToken, userRole } = useAuthorized(); const [createClicked, setCreateClicked] = useState(false); const queryClient = new QueryClient(); diff --git a/ui/litellm-dashboard/src/app/dashboard/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/dashboard/components/SidebarProvider.tsx deleted file mode 100644 index 8380493798..0000000000 --- a/ui/litellm-dashboard/src/app/dashboard/components/SidebarProvider.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import Sidebar2 from "@/app/dashboard/components/Sidebar2"; -import Sidebar from "@/components/leftnav"; -import React from "react"; -import useFeatureFlags from "@/hooks/useFeatureFlags"; -import useAuthorized from "@/app/dashboard/hooks/useAuthorized"; - -interface SidebarProviderProps { - defaultSelectedKey: string; - sidebarCollapsed: boolean; - setPage: (page: string) => void; -} - -const SidebarProvider = ({defaultSelectedKey, sidebarCollapsed, setPage}: SidebarProviderProps) => { - - const {accessToken, userRole} = useAuthorized(); - const { refactoredUIFlag, setRefactoredUIFlag } = useFeatureFlags(); - - return ( - refactoredUIFlag ? ( - - ) : ( - - ) - ); - -} - -export default SidebarProvider; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 3815df002b..8e25dc192c 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -40,8 +40,8 @@ import UIThemeSettings from "@/components/ui_theme_settings"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { cx } from "@/lib/cva.config"; import useFeatureFlags, { FeatureFlagsProvider } from "@/hooks/useFeatureFlags"; -import Sidebar2 from "@/app/dashboard/components/Sidebar2"; -import SidebarProvider from "@/app/dashboard/components/SidebarProvider"; +import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; +import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider"; function getCookie(name: string) { const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); @@ -126,6 +126,10 @@ export default function CreateKeyPage() { return searchParams.get("page") || "api-keys"; }); + useEffect(() => { + setPage(searchParams.get("page") || "api-keys"); + }, [searchParams]); + // Custom setPage function that updates URL const updatePage = (newPage: string) => { // Update URL without full page reload diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 34916cddb9..a0d8d162c6 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -56,6 +56,17 @@ 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[] = [ @@ -214,6 +225,7 @@ 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 @@ -248,6 +260,15 @@ 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: () => { - const newSearchParams = new URLSearchParams(window.location.search); - newSearchParams.set("page", child.page); - window.history.pushState(null, "", `?${newSearchParams.toString()}`); - setPage(child.page); - }, + onClick: () => goTo(child.page), })), - 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, + onClick: !item.children ? () => goTo(item.page) : undefined, }))} /> diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index d4035e83f0..c37d94abb1 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -131,11 +131,10 @@ export const fetchUserModels = async ( } }; - /** * ───────────────────────────────────────────────────────────────────────── * @deprecated - * This component is being DEPRECATED in favor of src/app/dashboard/virtual-keys/components/CreateKey.tsx + * This component is being DEPRECATED in favor of src/app/(dashboard)/virtual-keys/components/CreateKey.tsx * Please contribute to the new refactor. * ───────────────────────────────────────────────────────────────────────── */ diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index ba04e2644a..26adaa11db 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -50,7 +50,7 @@ interface KeyInfoViewProps { /** * ───────────────────────────────────────────────────────────────────────── * @deprecated - * This component is being DEPRECATED in favor of src/app/dashboard/virtual-keys/components/KeyInfoView.tsx + * This component is being DEPRECATED in favor of src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx * Please contribute to the new refactor. * ───────────────────────────────────────────────────────────────────────── */ diff --git a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx index 3fad7c0a35..d146d5ba79 100644 --- a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx @@ -138,7 +138,7 @@ interface CombinedLimits { /** * ───────────────────────────────────────────────────────────────────────── * @deprecated - * This component is being DEPRECATED in favor of src/app/dashboard/virtual-keys/components/VirtualKeysTable/ + * This component is being DEPRECATED in favor of src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/ * Please contribute to the new refactor. * ───────────────────────────────────────────────────────────────────────── */ diff --git a/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx b/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx index d50fdf46a6..f1914f463a 100644 --- a/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx +++ b/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx @@ -1,26 +1,87 @@ -'use client'; -import React, {createContext, useContext, useState} from 'react'; +"use client"; +import React, { createContext, useContext, useEffect, useState } from "react"; type Flags = { refactoredUIFlag: boolean; setRefactoredUIFlag: (v: boolean) => void; }; +const STORAGE_KEY = "feature.refactoredUIFlag"; + const FeatureFlagsCtx = createContext(null); -export const FeatureFlagsProvider = ({ children }: { children: React.ReactNode }) => { - const [refactoredUIFlag, setRefactoredUIFlag] = useState(false); - return ( - - {children} - -); +/** Safely read the flag from localStorage. If anything goes wrong, reset to false. */ +function readFlagSafely(): boolean { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw === null) { + localStorage.setItem(STORAGE_KEY, "false"); + return false; + } + + const v = raw.trim().toLowerCase(); + if (v === "true" || v === "1") return true; + if (v === "false" || v === "0") return false; + + // Last chance: try JSON.parse in case something odd was stored. + const parsed = JSON.parse(raw); + if (typeof parsed === "boolean") return parsed; + + // Malformed → reset to false + localStorage.setItem(STORAGE_KEY, "false"); + return false; + } catch { + // If even accessing localStorage throws, best effort reset then default to false + try { + localStorage.setItem(STORAGE_KEY, "false"); + } catch {} + return false; + } } +function writeFlagSafely(v: boolean) { + try { + localStorage.setItem(STORAGE_KEY, String(v)); + } catch { + // Ignore write errors; state will still reflect the intended value. + } +} + +export const FeatureFlagsProvider = ({ children }: { children: React.ReactNode }) => { + // Lazy init reads from localStorage only on the client + const [refactoredUIFlag, setRefactoredUIFlagState] = useState(() => readFlagSafely()); + + const setRefactoredUIFlag = (v: boolean) => { + setRefactoredUIFlagState(v); + writeFlagSafely(v); + }; + + // Keep this flag in sync across tabs/windows. + useEffect(() => { + const onStorage = (e: StorageEvent) => { + if (e.key === STORAGE_KEY && e.newValue != null) { + const next = e.newValue.trim().toLowerCase(); + setRefactoredUIFlagState(next === "true" || next === "1"); + } + // If the key was cleared elsewhere, self-heal to false. + if (e.key === STORAGE_KEY && e.newValue === null) { + writeFlagSafely(false); + setRefactoredUIFlagState(false); + } + }; + window.addEventListener("storage", onStorage); + return () => window.removeEventListener("storage", onStorage); + }, []); + + return ( + {children} + ); +}; + const useFeatureFlags = () => { const ctx = useContext(FeatureFlagsCtx); - if (!ctx) throw new Error('useFeatureFlags must be used within FeatureFlagsProvider'); + if (!ctx) throw new Error("useFeatureFlags must be used within FeatureFlagsProvider"); return ctx; -} +}; export default useFeatureFlags; From eed19fb4abc6f1c68fa4680acf9436f8fab06869 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Mon, 6 Oct 2025 19:36:40 -0700 Subject: [PATCH 10/17] slight improvement for Sidebar2 --- .../app/(dashboard)/components/Sidebar2.tsx | 60 ++++++++++++------- ui/litellm-dashboard/src/app/page.tsx | 1 - 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx index 209cf2e67f..37901b5e63 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx @@ -1,8 +1,7 @@ "use client"; import React from "react"; -import Link from "next/link"; -import { usePathname, useSearchParams } from "next/navigation"; +import { usePathname, useSearchParams, useRouter } from "next/navigation"; import { Layout, Menu, ConfigProvider } from "antd"; import { KeyOutlined, @@ -62,6 +61,11 @@ const withBase = (relativePath: string) => { }; const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + /** ---------- Menu model ---------- */ const menuItems: MenuItem[] = [ { key: "1", page: "api-keys", label: "Virtual Keys", icon: }, { @@ -217,9 +221,7 @@ const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelect children: item.children?.filter((child) => !child.roles || child.roles.includes(userRole)), })); - // Highlight selection based on pathname or ?page= - const pathname = usePathname(); - const searchParams = useSearchParams(); + /** ---------- Selection state ---------- */ const pageParam = searchParams.get("page") || undefined; const findMenuItemKey = (page: string): string => { @@ -244,31 +246,47 @@ const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelect ? findMenuItemKey(defaultSelectedKey) : "1"; - // Root-only routing helper: always replace everything after the domain, honoring base path - const rootWithPage = (p: string) => ({ - pathname: getBasePath() || "/", - query: { page: p }, - }); + /** ---------- Navigation helpers (SPA only) ---------- */ + // Build a root URL ("/" or "/base/") with an updated ?page=... + const goTo = (p: string) => { + const base = getBasePath() || "/"; + const root = base.endsWith("/") ? base : `${base}/`; + const sp = new URLSearchParams(typeof window !== "undefined" ? window.location.search : ""); + sp.set("page", p); + // Use Next router for client navigation on the SAME route (no hard fetch) + router.replace(`${root}?${sp.toString()}`, { scroll: false }); + }; - // Convert to AntD Menu items: - // - "Virtual Keys" routes to "//virtual-keys" - // - All other items (and children) route to "//?page=" + // Keep the /virtual-keys path the same, but avoid route fetches/hard reloads. + const goToVirtualKeys = () => { + const base = getBasePath() || "/"; + const root = base.endsWith("/") ? base : `${base}/`; + const sp = new URLSearchParams(typeof window !== "undefined" ? window.location.search : ""); + sp.set("page", "api-keys"); + + // 1) Client transition to the root with ?page=api-keys so the view updates. + router.replace(`${root}?${sp.toString()}`, { scroll: false }); + + // 2) Cosmetic URL swap to ".../virtual-keys" without navigation (keeps path the same). + const vk = withBase("virtual-keys"); + if (typeof window !== "undefined") { + window.history.replaceState(null, "", vk); + } + }; + + /** ---------- AntD items with onClick handlers ---------- */ const antdItems = filteredMenuItems.map((item) => { const isVirtualKeys = item.key === "1"; - const label = isVirtualKeys ? ( - Virtual Keys - ) : ( - {item.label} - ); - return { key: item.key, icon: item.icon, - label, + label: item.label, // plain text; click handled via onClick + onClick: !item.children ? (isVirtualKeys ? goToVirtualKeys : () => goTo(item.page)) : undefined, children: item.children?.map((child) => ({ key: child.key, icon: child.icon, - label: {child.label}, + label: child.label, + onClick: () => goTo(child.page), })), }; }); diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 8e25dc192c..104ef41810 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -39,7 +39,6 @@ import VectorStoreManagement from "@/components/vector_store_management"; import UIThemeSettings from "@/components/ui_theme_settings"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { cx } from "@/lib/cva.config"; -import useFeatureFlags, { FeatureFlagsProvider } from "@/hooks/useFeatureFlags"; import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider"; From 051c9a7c6192f5179e867ace63358f1bb67816a6 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Mon, 6 Oct 2025 20:17:19 -0700 Subject: [PATCH 11/17] Sidebar2 uses FF now --- .../app/(dashboard)/components/Sidebar2.tsx | 247 ++++++++---------- .../components/SidebarProvider.tsx | 32 --- .../src/app/(dashboard)/layout.tsx | 26 +- ui/litellm-dashboard/src/app/page.tsx | 18 +- 4 files changed, 141 insertions(+), 182 deletions(-) delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx index 37901b5e63..843a176f6e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx @@ -1,8 +1,5 @@ -"use client"; - -import React from "react"; -import { usePathname, useSearchParams, useRouter } from "next/navigation"; -import { Layout, Menu, ConfigProvider } from "antd"; +import { Layout, Menu } from "antd"; +import { useRouter, usePathname } from "next/navigation"; // UPDATED: also usePathname to detect current route import { KeyOutlined, PlayCircleOutlined, @@ -23,16 +20,18 @@ import { TagsOutlined, BgColorsOutlined, } from "@ant-design/icons"; +import { ConfigProvider } from "antd"; +import useFeatureFlags from "@/hooks/useFeatureFlags"; import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "@/utils/roles"; import UsageIndicator from "@/components/usage_indicator"; - +import React from "react"; const { Sider } = Layout; interface SidebarProps { accessToken: string | null; + setPage: (page: string) => void; userRole: string; - /** Used to highlight a menu item when pathname doesn't match (e.g., on non-routed pages) */ - defaultSelectedKey?: string; + defaultSelectedKey: string; collapsed?: boolean; } @@ -45,87 +44,77 @@ interface MenuItem { icon?: React.ReactNode; } -/** ---------- Base URL helpers ---------- */ -/** Normalizes NEXT_PUBLIC_BASE_URL into either "" or "/something" (no trailing slash). */ -const getBasePath = () => { - const raw = process.env.NEXT_PUBLIC_BASE_URL ?? ""; - const trimmed = raw.replace(/^\/+|\/+$/g, ""); // strip leading/trailing slashes - return trimmed ? `/${trimmed}` : ""; -}; - -/** Joins base path with a relative path like "virtual-keys" or "/virtual-keys" -> "/base/virtual-keys" */ -const withBase = (relativePath: string) => { - const base = getBasePath(); // "" or "/ui" (no trailing slash) - const rel = relativePath.replace(/^\/+/, ""); // drop any leading slash - return `${base}/${rel}`.replace(/\/{2,}/g, "/"); // collapse accidental doubles -}; - -const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => { +const Sidebar2: React.FC = ({ + accessToken, + setPage, + userRole, + defaultSelectedKey, + collapsed = false, +}) => { const router = useRouter(); const pathname = usePathname(); - const searchParams = useSearchParams(); + const { refactoredUIFlag } = useFeatureFlags(); - /** ---------- Menu model ---------- */ const menuItems: MenuItem[] = [ - { key: "1", page: "api-keys", label: "Virtual Keys", icon: }, + { key: "1", page: "api-keys", label: "Virtual Keys", icon: }, { key: "3", page: "llm-playground", label: "Test Key", - icon: , + icon: , roles: rolesWithWriteAccess, }, { key: "2", page: "models", label: "Models + Endpoints", - icon: , + icon: , roles: rolesWithWriteAccess, }, { key: "12", page: "new_usage", label: "Usage", - icon: , + icon: , roles: [...all_admin_roles, ...internalUserRoles], }, - { key: "6", page: "teams", label: "Teams", icon: }, + { key: "6", page: "teams", label: "Teams", icon: }, { key: "17", page: "organizations", label: "Organizations", - icon: , + icon: , roles: all_admin_roles, }, { key: "5", page: "users", label: "Internal Users", - icon: , + icon: , roles: all_admin_roles, }, - { key: "14", page: "api_ref", label: "API Reference", icon: }, - { key: "16", page: "model-hub-table", label: "Model Hub", icon: }, - { key: "15", page: "logs", label: "Logs", icon: }, + { key: "14", page: "api_ref", label: "API Reference", icon: }, + { key: "16", page: "model-hub-table", label: "Model Hub", icon: }, + { key: "15", page: "logs", label: "Logs", icon: }, { key: "11", page: "guardrails", label: "Guardrails", - icon: , + icon: , roles: all_admin_roles, }, { key: "26", page: "tools", label: "Tools", - icon: , + icon: , children: [ - { key: "18", page: "mcp-servers", label: "MCP Servers", icon: }, + { key: "18", page: "mcp-servers", label: "MCP Servers", icon: }, { key: "21", page: "vector-stores", label: "Vector Stores", - icon: , + icon: , roles: all_admin_roles, }, ], @@ -134,162 +123,145 @@ const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelect key: "experimental", page: "experimental", label: "Experimental", - icon: , + icon: , children: [ { key: "9", page: "caching", label: "Caching", - icon: , + icon: , roles: all_admin_roles, }, { key: "25", page: "prompts", label: "Prompts", - icon: , + icon: , roles: all_admin_roles, }, { key: "10", page: "budgets", label: "Budgets", - icon: , + icon: , roles: all_admin_roles, }, { key: "20", page: "transform-request", label: "API Playground", - icon: , + icon: , roles: [...all_admin_roles, ...internalUserRoles], }, { key: "19", page: "tag-management", label: "Tag Management", - icon: , + icon: , roles: all_admin_roles, }, - { key: "4", page: "usage", label: "Old Usage", icon: }, + { key: "4", page: "usage", label: "Old Usage", icon: }, ], }, { key: "settings", page: "settings", label: "Settings", - icon: , + icon: , roles: all_admin_roles, children: [ { key: "11", page: "general-settings", label: "Router Settings", - icon: , + icon: , roles: all_admin_roles, }, { key: "8", page: "settings", label: "Logging & Alerts", - icon: , + icon: , roles: all_admin_roles, }, { key: "13", page: "admin-panel", label: "Admin Settings", - icon: , + icon: , roles: all_admin_roles, }, { key: "14", page: "ui-theme", label: "UI Theme", - icon: , + icon: , roles: all_admin_roles, }, ], }, ]; - // Role filtering (preserves original visibility behavior) - const filteredMenuItems: MenuItem[] = menuItems - .filter((item) => !item.roles || item.roles.includes(userRole)) - .map((item) => ({ - ...item, - children: item.children?.filter((child) => !child.roles || child.roles.includes(userRole)), - })); - - /** ---------- Selection state ---------- */ - const pageParam = searchParams.get("page") || undefined; - const findMenuItemKey = (page: string): string => { - const top = filteredMenuItems.find((i) => i.page === page); - if (top) return top.key; - for (const i of filteredMenuItems) { - const child = i.children?.find((c) => c.page === page); - if (child) return child.key; + const topLevelItem = menuItems.find((item) => item.page === page); + if (topLevelItem) return topLevelItem.key; + for (const item of menuItems) { + if (item.children) { + const childItem = item.children.find((child) => child.page === page); + if (childItem) return childItem.key; + } } return "1"; }; - // Match Virtual Keys path with base prefix (e.g., "/virtual-keys" or "/ui/virtual-keys") - const virtualKeysPath = withBase("virtual-keys"); + const selectedMenuKey = findMenuItemKey(defaultSelectedKey); - const selectedMenuKey = - pathname === virtualKeysPath - ? "1" - : pageParam - ? findMenuItemKey(pageParam) - : defaultSelectedKey - ? findMenuItemKey(defaultSelectedKey) - : "1"; + const filteredMenuItems = menuItems.filter((item) => { + const hasParentAccess = !item.roles || item.roles.includes(userRole); + if (!hasParentAccess) return false; + if (item.children) { + item.children = item.children.filter((child) => !child.roles || child.roles.includes(userRole)); + } + return true; + }); - /** ---------- Navigation helpers (SPA only) ---------- */ - // Build a root URL ("/" or "/base/") with an updated ?page=... - const goTo = (p: string) => { - const base = getBasePath() || "/"; - const root = base.endsWith("/") ? base : `${base}/`; - const sp = new URLSearchParams(typeof window !== "undefined" ? window.location.search : ""); - sp.set("page", p); - // Use Next router for client navigation on the SAME route (no hard fetch) - router.replace(`${root}?${sp.toString()}`, { scroll: false }); - }; - - // Keep the /virtual-keys path the same, but avoid route fetches/hard reloads. - const goToVirtualKeys = () => { - const base = getBasePath() || "/"; - const root = base.endsWith("/") ? base : `${base}/`; - const sp = new URLSearchParams(typeof window !== "undefined" ? window.location.search : ""); - sp.set("page", "api-keys"); - - // 1) Client transition to the root with ?page=api-keys so the view updates. - router.replace(`${root}?${sp.toString()}`, { scroll: false }); - - // 2) Cosmetic URL swap to ".../virtual-keys" without navigation (keeps path the same). - const vk = withBase("virtual-keys"); - if (typeof window !== "undefined") { - window.history.replaceState(null, "", vk); + // Helper: go to /?page=...; when on /virtual-keys with refactored UI, replace instead of appending + const pushToRootWithPage = (page: string, useReplace = false) => { + const params = new URLSearchParams(); + params.set("page", page); + const url = `/?${params.toString()}`; + if (useReplace) { + router.replace(url); + } else { + router.push(url); } }; - /** ---------- AntD items with onClick handlers ---------- */ - const antdItems = filteredMenuItems.map((item) => { - const isVirtualKeys = item.key === "1"; - return { - key: item.key, - icon: item.icon, - label: item.label, // plain text; click handled via onClick - onClick: !item.children ? (isVirtualKeys ? goToVirtualKeys : () => goTo(item.page)) : undefined, - children: item.children?.map((child) => ({ - key: child.key, - icon: child.icon, - label: child.label, - onClick: () => goTo(child.page), - })), - }; - }); + // Centralized navigation that satisfies the requirement + const navigateToPage = (page: string) => { + // Special-case: Virtual Keys target + if (page === "api-keys") { + if (refactoredUIFlag) { + // Go to the dedicated /virtual-keys page + router.push("/virtual-keys"); + return; // do not call setPage here (parity with previous behavior) + } + // Legacy behavior + pushToRootWithPage(page); + setPage(page); + return; + } + + // All other pages + if (refactoredUIFlag) { + // If currently on /virtual-keys, REPLACE it with /?page=... + const onVirtualKeys = pathname?.startsWith("/virtual-keys"); + pushToRootWithPage(page, onVirtualKeys); + } else { + pushToRootWithPage(page); + } + setPage(page); + }; return ( @@ -300,29 +272,36 @@ const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelect collapsedWidth={80} collapsible trigger={null} - style={{ - transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", - position: "relative", - }} + style={{ transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", position: "relative" }} > - + ({ + key: item.key, + icon: item.icon, + label: item.label, + children: item.children?.map((child) => ({ + key: child.key, + icon: child.icon, + label: child.label, + onClick: () => { + navigateToPage(child.page); + }, + })), + onClick: !item.children + ? () => { + navigateToPage(item.page); + } + : undefined, + }))} /> - {isAdminRole(userRole) && !collapsed && } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx deleted file mode 100644 index 822a30e640..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; -import Sidebar from "@/components/leftnav"; -import React from "react"; -import useFeatureFlags from "@/hooks/useFeatureFlags"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -interface SidebarProviderProps { - defaultSelectedKey: string; - sidebarCollapsed: boolean; - setPage: (page: string) => void; -} - -const SidebarProvider = ({ defaultSelectedKey, sidebarCollapsed, setPage }: SidebarProviderProps) => { - const { accessToken, userRole } = useAuthorized(); - const { refactoredUIFlag, setRefactoredUIFlag } = useFeatureFlags(); - - return refactoredUIFlag ? ( - - ) : ( - - ); -}; - -export default SidebarProvider; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx index ad8a82703f..7e60443d24 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx @@ -1,14 +1,31 @@ "use client"; -import React from "react"; +import React, { useEffect, useState } from "react"; import Navbar from "@/components/navbar"; import { ThemeProvider } from "@/contexts/ThemeContext"; import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useRouter, useSearchParams } from "next/navigation"; export default function Layout({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const searchParams = useSearchParams(); const { accessToken, userRole } = useAuthorized(); const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false); + const [page, setPage] = useState(() => { + return searchParams.get("page") || "api-keys"; + }); + + const updatePage = (newPage: string) => { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("page", newPage); + router.push(`/?${newSearchParams.toString()}`); // absolute, not relative + setPage(newPage); + }; + + useEffect(() => { + setPage(searchParams.get("page") || "api-keys"); + }, [searchParams]); const toggleSidebar = () => setSidebarCollapsed((v) => !v); @@ -31,12 +48,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { />
- +
{children}
diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 104ef41810..d386df4b77 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -1,7 +1,7 @@ "use client"; import React, { Suspense, useEffect, useState } from "react"; -import { useSearchParams } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { jwtDecode } from "jwt-decode"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Team } from "@/components/key_team_helpers/key_list"; @@ -23,7 +23,6 @@ import ModelHubTable from "@/components/model_hub_table"; import NewUsagePage from "@/components/new_usage"; import APIRef from "@/components/api_ref"; import ChatUI from "@/components/chat_ui/ChatUI"; -import Sidebar from "@/components/leftnav"; import Usage from "@/components/usage"; import CacheDashboard from "@/components/cache_dashboard"; import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking"; @@ -40,7 +39,6 @@ import UIThemeSettings from "@/components/ui_theme_settings"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { cx } from "@/lib/cva.config"; import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; -import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider"; function getCookie(name: string) { const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); @@ -97,6 +95,7 @@ function LoadingScreen() { } export default function CreateKeyPage() { + const router = useRouter(); const [userRole, setUserRole] = useState(""); const [premiumUser, setPremiumUser] = useState(false); const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false); @@ -131,13 +130,9 @@ export default function CreateKeyPage() { // Custom setPage function that updates URL const updatePage = (newPage: string) => { - // Update URL without full page reload const newSearchParams = new URLSearchParams(searchParams); newSearchParams.set("page", newPage); - - // Use Next.js router to update URL - window.history.pushState(null, "", `?${newSearchParams.toString()}`); - + router.push(`/?${newSearchParams.toString()}`); // absolute, not relative setPage(newPage); }; @@ -264,7 +259,12 @@ export default function CreateKeyPage() { />
- +
{page == "api-keys" ? ( From cc94fb87b5179936bf02cede86617eb043fbc2b4 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Mon, 6 Oct 2025 20:32:47 -0700 Subject: [PATCH 12/17] added base URL helpers --- .../app/(dashboard)/components/Sidebar2.tsx | 28 +++++++++++++++---- .../src/app/(dashboard)/layout.tsx | 17 ++++++++++- ui/litellm-dashboard/src/app/page.tsx | 20 +++++++++++-- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx index 843a176f6e..95907f3def 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx @@ -44,6 +44,24 @@ interface MenuItem { icon?: React.ReactNode; } +/** ---- BASE URL HELPERS (shared pattern across files) ---- */ +function normalizeBasePrefix(raw: string | undefined | null): string { + const trimmed = (raw ?? "").trim(); + if (!trimmed) return ""; // no base + // strip leading/trailing slashes then rebuild as "/segment/" + const core = trimmed.replace(/^\/+/, "").replace(/\/+$/, ""); + return core ? `/${core}/` : "/"; +} +const BASE_PREFIX = normalizeBasePrefix(process.env.NEXT_PUBLIC_BASE_URL); +/** Builds an app-relative path under the configured base prefix. */ +function withBase(path: string): string { + // Accepts paths like "/virtual-keys" or "/?page=..." and prefixes with BASE_PREFIX when present. + const body = path.startsWith("/") ? path.slice(1) : path; + const combined = `${BASE_PREFIX}${body}`; + return combined.startsWith("/") ? combined : `/${combined}`; +} +/** -------------------------------------------------------- */ + const Sidebar2: React.FC = ({ accessToken, setPage, @@ -229,7 +247,7 @@ const Sidebar2: React.FC = ({ const pushToRootWithPage = (page: string, useReplace = false) => { const params = new URLSearchParams(); params.set("page", page); - const url = `/?${params.toString()}`; + const url = withBase(`/?${params.toString()}`); if (useReplace) { router.replace(url); } else { @@ -242,8 +260,8 @@ const Sidebar2: React.FC = ({ // Special-case: Virtual Keys target if (page === "api-keys") { if (refactoredUIFlag) { - // Go to the dedicated /virtual-keys page - router.push("/virtual-keys"); + // Go to the dedicated /virtual-keys page (always under base) + router.push(withBase("/virtual-keys")); return; // do not call setPage here (parity with previous behavior) } // Legacy behavior @@ -255,8 +273,8 @@ const Sidebar2: React.FC = ({ // All other pages if (refactoredUIFlag) { // If currently on /virtual-keys, REPLACE it with /?page=... - const onVirtualKeys = pathname?.startsWith("/virtual-keys"); - pushToRootWithPage(page, onVirtualKeys); + const onVirtualKeys = pathname?.startsWith(withBase("/virtual-keys")); + pushToRootWithPage(page, !!onVirtualKeys); } else { pushToRootWithPage(page); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx index 7e60443d24..3d38a7362b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx @@ -7,6 +7,21 @@ import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { useRouter, useSearchParams } from "next/navigation"; +/** ---- BASE URL HELPERS ---- */ +function normalizeBasePrefix(raw: string | undefined | null): string { + const trimmed = (raw ?? "").trim(); + if (!trimmed) return ""; + const core = trimmed.replace(/^\/+/, "").replace(/\/+$/, ""); + return core ? `/${core}/` : "/"; +} +const BASE_PREFIX = normalizeBasePrefix(process.env.NEXT_PUBLIC_BASE_URL); +function withBase(path: string): string { + const body = path.startsWith("/") ? path.slice(1) : path; + const combined = `${BASE_PREFIX}${body}`; + return combined.startsWith("/") ? combined : `/${combined}`; +} +/** -------------------------------- */ + export default function Layout({ children }: { children: React.ReactNode }) { const router = useRouter(); const searchParams = useSearchParams(); @@ -19,7 +34,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { const updatePage = (newPage: string) => { const newSearchParams = new URLSearchParams(searchParams); newSearchParams.set("page", newPage); - router.push(`/?${newSearchParams.toString()}`); // absolute, not relative + router.push(withBase(`/?${newSearchParams.toString()}`)); // always under BASE setPage(newPage); }; diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index d386df4b77..b110858349 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -40,6 +40,21 @@ import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { cx } from "@/lib/cva.config"; import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; +/** ---- BASE URL HELPERS ---- */ +function normalizeBasePrefix(raw: string | undefined | null): string { + const trimmed = (raw ?? "").trim(); + if (!trimmed) return ""; + const core = trimmed.replace(/^\/+/, "").replace(/\/+$/, ""); + return core ? `/${core}/` : "/"; +} +const BASE_PREFIX = normalizeBasePrefix(process.env.NEXT_PUBLIC_BASE_URL); +function withBase(path: string): string { + const body = path.startsWith("/") ? path.slice(1) : path; + const combined = `${BASE_PREFIX}${body}`; + return combined.startsWith("/") ? combined : `/${combined}`; +} +/** -------------------------------- */ + function getCookie(name: string) { const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); return cookieValue ? cookieValue.split("=")[1] : null; @@ -128,11 +143,11 @@ export default function CreateKeyPage() { setPage(searchParams.get("page") || "api-keys"); }, [searchParams]); - // Custom setPage function that updates URL + // Custom setPage function that updates URL (always under BASE) const updatePage = (newPage: string) => { const newSearchParams = new URLSearchParams(searchParams); newSearchParams.set("page", newPage); - router.push(`/?${newSearchParams.toString()}`); // absolute, not relative + router.push(withBase(`/?${newSearchParams.toString()}`)); setPage(newPage); }; @@ -160,6 +175,7 @@ export default function CreateKeyPage() { useEffect(() => { if (redirectToLogin) { + // This is an external auth route (kept as-is, not app navigation). window.location.href = (proxyBaseUrl || "") + "/sso/key/generate"; } }, [redirectToLogin]); From 7ed4715a9b763faf15520989637f99f62828989c Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Mon, 6 Oct 2025 20:42:59 -0700 Subject: [PATCH 13/17] eliminates loading screen flash --- .../app/(dashboard)/components/Sidebar2.tsx | 61 +- ui/litellm-dashboard/src/app/page.tsx | 534 +++++++++--------- 2 files changed, 280 insertions(+), 315 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx index 95907f3def..2ebfbc5d4f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx @@ -1,5 +1,5 @@ import { Layout, Menu } from "antd"; -import { useRouter, usePathname } from "next/navigation"; // UPDATED: also usePathname to detect current route +import { usePathname } from "next/navigation"; import { KeyOutlined, PlayCircleOutlined, @@ -44,23 +44,29 @@ interface MenuItem { icon?: React.ReactNode; } -/** ---- BASE URL HELPERS (shared pattern across files) ---- */ +/** ---- BASE URL HELPERS ---- */ function normalizeBasePrefix(raw: string | undefined | null): string { const trimmed = (raw ?? "").trim(); if (!trimmed) return ""; // no base - // strip leading/trailing slashes then rebuild as "/segment/" const core = trimmed.replace(/^\/+/, "").replace(/\/+$/, ""); - return core ? `/${core}/` : "/"; + return core ? `/${core}` : ""; } const BASE_PREFIX = normalizeBasePrefix(process.env.NEXT_PUBLIC_BASE_URL); -/** Builds an app-relative path under the configured base prefix. */ + +/** Build an absolute path under the configured base. */ function withBase(path: string): string { - // Accepts paths like "/virtual-keys" or "/?page=..." and prefixes with BASE_PREFIX when present. - const body = path.startsWith("/") ? path.slice(1) : path; - const combined = `${BASE_PREFIX}${body}`; - return combined.startsWith("/") ? combined : `/${combined}`; + // path can be "/virtual-keys" or "/?page=..." + const p = path.startsWith("/") ? path : `/${path}`; + return `${BASE_PREFIX}${p}` || p; } -/** -------------------------------------------------------- */ + +/** History-based navigation to prevent hard reloads / suspense flashes */ +function softNavigate(url: string, replace = false) { + if (typeof window === "undefined") return; + if (replace) window.history.replaceState(null, "", url); + else window.history.pushState(null, "", url); +} +/** -------------------------------- */ const Sidebar2: React.FC = ({ accessToken, @@ -69,7 +75,6 @@ const Sidebar2: React.FC = ({ defaultSelectedKey, collapsed = false, }) => { - const router = useRouter(); const pathname = usePathname(); const { refactoredUIFlag } = useFeatureFlags(); @@ -243,38 +248,30 @@ const Sidebar2: React.FC = ({ return true; }); - // Helper: go to /?page=...; when on /virtual-keys with refactored UI, replace instead of appending + // Helper: update /?page=... under the configured base, WITHOUT triggering App Router nav const pushToRootWithPage = (page: string, useReplace = false) => { const params = new URLSearchParams(); params.set("page", page); const url = withBase(`/?${params.toString()}`); - if (useReplace) { - router.replace(url); - } else { - router.push(url); - } + softNavigate(url, useReplace); }; - // Centralized navigation that satisfies the requirement const navigateToPage = (page: string) => { - // Special-case: Virtual Keys target if (page === "api-keys") { if (refactoredUIFlag) { - // Go to the dedicated /virtual-keys page (always under base) - router.push(withBase("/virtual-keys")); - return; // do not call setPage here (parity with previous behavior) + // vanity URL, keep SPA alive + softNavigate(withBase("/virtual-keys")); + return; // don't call setPage to keep parity, UI already shows api-keys by default } - // Legacy behavior pushToRootWithPage(page); setPage(page); return; } - // All other pages if (refactoredUIFlag) { - // If currently on /virtual-keys, REPLACE it with /?page=... - const onVirtualKeys = pathname?.startsWith(withBase("/virtual-keys")); - pushToRootWithPage(page, !!onVirtualKeys); + const onVirtualKeys = + typeof window !== "undefined" && window.location.pathname.startsWith(withBase("/virtual-keys")); + pushToRootWithPage(page, onVirtualKeys); } else { pushToRootWithPage(page); } @@ -308,15 +305,9 @@ const Sidebar2: React.FC = ({ key: child.key, icon: child.icon, label: child.label, - onClick: () => { - navigateToPage(child.page); - }, + onClick: () => navigateToPage(child.page), })), - onClick: !item.children - ? () => { - navigateToPage(item.page); - } - : undefined, + onClick: !item.children ? () => navigateToPage(item.page) : undefined, }))} /> diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index b110858349..f83972e0c1 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -1,7 +1,6 @@ "use client"; -import React, { Suspense, useEffect, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import React, { useEffect, useMemo, useState } from "react"; import { jwtDecode } from "jwt-decode"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Team } from "@/components/key_team_helpers/key_list"; @@ -45,32 +44,28 @@ function normalizeBasePrefix(raw: string | undefined | null): string { const trimmed = (raw ?? "").trim(); if (!trimmed) return ""; const core = trimmed.replace(/^\/+/, "").replace(/\/+$/, ""); - return core ? `/${core}/` : "/"; + return core ? `/${core}` : ""; } const BASE_PREFIX = normalizeBasePrefix(process.env.NEXT_PUBLIC_BASE_URL); function withBase(path: string): string { - const body = path.startsWith("/") ? path.slice(1) : path; - const combined = `${BASE_PREFIX}${body}`; - return combined.startsWith("/") ? combined : `/${combined}`; + const p = path.startsWith("/") ? path : `/${path}`; + return `${BASE_PREFIX}${p}` || p; } /** -------------------------------- */ function getCookie(name: string) { + if (typeof document === "undefined") return null; const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); return cookieValue ? cookieValue.split("=")[1] : null; } function formatUserRole(userRole: string) { - if (!userRole) { - return "Undefined Role"; - } + if (!userRole) return "Undefined Role"; switch (userRole.toLowerCase()) { case "app_owner": - return "App Owner"; case "demo_app_owner": return "App Owner"; case "app_admin": - return "Admin"; case "proxy_admin": return "Admin"; case "proxy_admin_viewer": @@ -80,7 +75,7 @@ function formatUserRole(userRole: string) { case "internal_user": return "Internal User"; case "internal_user_viewer": - case "internal_viewer": // TODO:remove if deprecated + case "internal_viewer": return "Internal Viewer"; case "app_user": return "App User"; @@ -100,7 +95,6 @@ function LoadingScreen() { return (
🚅 LiteLLM
-
Loading... @@ -109,8 +103,17 @@ function LoadingScreen() { ); } +/** Derive the app "page" from the URL without triggering App Router navigations */ +function getPageFromLocation(loc: Location): string { + const sp = new URLSearchParams(loc.search); + const p = sp.get("page"); + if (p) return p; + // vanity route for refactored UI + if (loc.pathname.endsWith("/virtual-keys")) return "api-keys"; + return "api-keys"; +} + export default function CreateKeyPage() { - const router = useRouter(); const [userRole, setUserRole] = useState(""); const [premiumUser, setPremiumUser] = useState(false); const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false); @@ -119,55 +122,64 @@ export default function CreateKeyPage() { const [keys, setKeys] = useState([]); const [organizations, setOrganizations] = useState([]); const [userModels, setUserModels] = useState([]); - const [proxySettings, setProxySettings] = useState({ - PROXY_BASE_URL: "", - PROXY_LOGOUT_URL: "", - }); - + const [proxySettings, setProxySettings] = useState({ PROXY_BASE_URL: "", PROXY_LOGOUT_URL: "" }); const [showSSOBanner, setShowSSOBanner] = useState(true); - const searchParams = useSearchParams()!; - const [modelData, setModelData] = useState({ data: [] }); + + // Stable local mirror of URLSearchParams (no suspense, no flashes) + const [searchParams, setSearchParams] = useState(() => + typeof window === "undefined" ? new URLSearchParams() : new URLSearchParams(window.location.search), + ); + const invitation_id = useMemo(() => searchParams.get("invitation_id"), [searchParams]); + + // Local page state (drives UI) + const [page, setPage] = useState(() => + typeof window === "undefined" ? "api-keys" : getPageFromLocation(window.location), + ); + + // Keep history/back/forward in sync with UI + useEffect(() => { + const onPop = () => { + if (typeof window === "undefined") return; + setSearchParams(new URLSearchParams(window.location.search)); + setPage(getPageFromLocation(window.location)); + }; + window.addEventListener("popstate", onPop); + return () => window.removeEventListener("popstate", onPop); + }, []); + + // Update URL without triggering App Router reload/suspense + const updatePage = (newPage: string) => { + if (typeof window === "undefined") return; + // 1) instant UI update + setPage(newPage); + // 2) URL update under base prefix + const sp = new URLSearchParams(window.location.search); + sp.set("page", newPage); + const url = withBase(`/?${sp.toString()}`); + window.history.pushState(null, "", url); + // 3) keep our local mirror in sync + setSearchParams(new URLSearchParams(sp)); + }; + const [token, setToken] = useState(null); const [createClicked, setCreateClicked] = useState(false); const [authLoading, setAuthLoading] = useState(true); const [userID, setUserID] = useState(null); - const invitation_id = searchParams.get("invitation_id"); - - // Get page from URL, default to 'api-keys' if not present - const [page, setPage] = useState(() => { - return searchParams.get("page") || "api-keys"; - }); - - useEffect(() => { - setPage(searchParams.get("page") || "api-keys"); - }, [searchParams]); - - // Custom setPage function that updates URL (always under BASE) - const updatePage = (newPage: string) => { - const newSearchParams = new URLSearchParams(searchParams); - newSearchParams.set("page", newPage); - router.push(withBase(`/?${newSearchParams.toString()}`)); - setPage(newPage); - }; - const [accessToken, setAccessToken] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - const toggleSidebar = () => { - setSidebarCollapsed(!sidebarCollapsed); - }; + const toggleSidebar = () => setSidebarCollapsed((v) => !v); const addKey = (data: any) => { setKeys((prevData) => (prevData ? [...prevData, data] : [data])); - setCreateClicked(() => !createClicked); + setCreateClicked((v) => !v); }; const redirectToLogin = authLoading === false && token === null && invitation_id === null; useEffect(() => { const token = getCookie("token"); - getUiConfig().then((data) => { - // get the information for constructing the proxy base url, and then set the token and auth loading + getUiConfig().then(() => { setToken(token); setAuthLoading(false); }); @@ -175,59 +187,33 @@ export default function CreateKeyPage() { useEffect(() => { if (redirectToLogin) { - // This is an external auth route (kept as-is, not app navigation). window.location.href = (proxyBaseUrl || "") + "/sso/key/generate"; } }, [redirectToLogin]); useEffect(() => { - if (!token) { - return; - } - + if (!token) return; const decoded = jwtDecode(token) as { [key: string]: any }; - if (decoded) { - // set accessToken - setAccessToken(decoded.key); + if (!decoded) return; - setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation); + setAccessToken(decoded.key); + setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation); - // check if userRole is defined - if (decoded.user_role) { - const formattedUserRole = formatUserRole(decoded.user_role); - setUserRole(formattedUserRole); - if (formattedUserRole == "Admin Viewer") { - setPage("usage"); - } - } - - if (decoded.user_email) { - setUserEmail(decoded.user_email); - } - - if (decoded.login_method) { - setShowSSOBanner(decoded.login_method == "username_password" ? true : false); - } - - if (decoded.premium_user) { - setPremiumUser(decoded.premium_user); - } - - if (decoded.auth_header_name) { - setGlobalLitellmHeaderName(decoded.auth_header_name); - } - - if (decoded.user_id) { - setUserID(decoded.user_id); - } + if (decoded.user_role) { + const formattedUserRole = formatUserRole(decoded.user_role); + setUserRole(formattedUserRole); + if (formattedUserRole === "Admin Viewer") setPage("usage"); } + if (decoded.user_email) setUserEmail(decoded.user_email); + if (decoded.login_method) setShowSSOBanner(decoded.login_method === "username_password"); + if (decoded.premium_user) setPremiumUser(decoded.premium_user); + if (decoded.auth_header_name) setGlobalLitellmHeaderName(decoded.auth_header_name); + if (decoded.user_id) setUserID(decoded.user_id); }, [token]); useEffect(() => { if (accessToken && userID && userRole) { fetchUserModels(userID, userRole, accessToken, setUserModels); - } - if (accessToken && userID && userRole) { fetchTeams(accessToken, userID, userRole, null, setTeams); } if (accessToken) { @@ -240,206 +226,194 @@ export default function CreateKeyPage() { } return ( - }> - - - {invitation_id ? ( - + + {invitation_id ? ( + + ) : ( +
+ - ) : ( -
- -
-
- -
- - {page == "api-keys" ? ( - - ) : page == "models" ? ( - - ) : page == "llm-playground" ? ( - - ) : page == "users" ? ( - - ) : page == "teams" ? ( - - ) : page == "organizations" ? ( - - ) : page == "admin-panel" ? ( - - ) : page == "api_ref" ? ( - - ) : page == "settings" ? ( - - ) : page == "budgets" ? ( - - ) : page == "guardrails" ? ( - - ) : page == "prompts" ? ( - - ) : page == "transform-request" ? ( - - ) : page == "general-settings" ? ( - - ) : page == "ui-theme" ? ( - - ) : page == "model-hub-table" ? ( - - ) : page == "caching" ? ( - - ) : page == "pass-through-settings" ? ( - - ) : page == "logs" ? ( - - ) : page == "mcp-servers" ? ( - - ) : page == "tag-management" ? ( - - ) : page == "vector-stores" ? ( - - ) : page == "new_usage" ? ( - - ) : ( - - )} +
+
+
+ + {page === "api-keys" ? ( + + ) : page === "models" ? ( + {}} + premiumUser={premiumUser} + teams={teams} + /> + ) : page === "llm-playground" ? ( + + ) : page === "users" ? ( + + ) : page === "teams" ? ( + + ) : page === "organizations" ? ( + + ) : page === "admin-panel" ? ( + + ) : page === "api_ref" ? ( + + ) : page === "settings" ? ( + + ) : page === "budgets" ? ( + + ) : page === "guardrails" ? ( + + ) : page === "prompts" ? ( + + ) : page === "transform-request" ? ( + + ) : page === "general-settings" ? ( + + ) : page === "ui-theme" ? ( + + ) : page === "model-hub-table" ? ( + + ) : page === "caching" ? ( + + ) : page === "pass-through-settings" ? ( + + ) : page === "logs" ? ( + + ) : page === "mcp-servers" ? ( + + ) : page === "tag-management" ? ( + + ) : page === "vector-stores" ? ( + + ) : page === "new_usage" ? ( + + ) : ( + + )}
- )} - - - +
+ )} + + ); } From 2b0df22dc549f8c394ea7864285569122e302b75 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Wed, 8 Oct 2025 17:07:46 -0700 Subject: [PATCH 14/17] next routing, feature flagged via sidebar --- .../app/(dashboard)/api-reference/page.tsx | 17 + .../app/(dashboard)/components/Sidebar2.tsx | 319 +++++---- .../components/SidebarProvider.tsx | 29 + .../components/modals/CreateUserModal.tsx | 2 +- .../experimental/api-playground/page.tsx | 12 + .../(dashboard)/experimental/budgets/page.tsx | 12 + .../(dashboard)/experimental/caching/page.tsx | 20 + .../experimental/old-usage/page.tsx | 23 + .../(dashboard)/experimental/prompts/page.tsx | 12 + .../experimental/tag-management/page.tsx | 12 + .../src/app/(dashboard)/guardrails/page.tsx | 12 + .../app/(dashboard)/hooks/useAuthorized.ts | 5 +- .../src/app/(dashboard)/logs/page.tsx | 28 + .../src/app/(dashboard)/model-hub/page.tsx | 12 + .../(dashboard)/models-and-endpoints/page.tsx | 29 + .../app/(dashboard)/organizations/page.tsx | 34 + .../settings/admin-settings/page.tsx | 29 + .../settings/logging-and-alerts/page.tsx | 12 + .../settings/router-settings/page.tsx | 12 + .../(dashboard)/settings/ui-theme/page.tsx | 12 + .../src/app/(dashboard)/teams/page.tsx | 35 + .../src/app/(dashboard)/test-key/page.tsx | 20 + .../(dashboard)/tools/mcp-servers/page.tsx | 19 + .../(dashboard)/tools/vector-stores/page.tsx | 12 + .../src/app/(dashboard)/usage/page.tsx | 22 + .../src/app/(dashboard)/users/page.tsx | 31 + .../VirtualKeysTable/VirtualKeysTable.tsx | 2 +- .../virtual-keys/hooks/useTeams.tsx | 4 +- ui/litellm-dashboard/src/app/page.tsx | 623 ++++++++++-------- 29 files changed, 1012 insertions(+), 399 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/api-reference/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/experimental/api-playground/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/experimental/budgets/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/experimental/caching/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/experimental/old-usage/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/experimental/prompts/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/experimental/tag-management/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/guardrails/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/model-hub/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/organizations/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/settings/logging-and-alerts/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/settings/router-settings/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/settings/ui-theme/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/test-key/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/tools/mcp-servers/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/tools/vector-stores/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/page.tsx new file mode 100644 index 0000000000..265a6d8195 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import APIRef from "@/components/api_ref"; +import { useState } from "react"; + +interface ProxySettings { + PROXY_BASE_URL: string; + PROXY_LOGOUT_URL: string; +} + +const APIReferencePage = () => { + const [proxySettings, setProxySettings] = useState({ PROXY_BASE_URL: "", PROXY_LOGOUT_URL: "" }); + + return ; +}; + +export default APIReferencePage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx index 2ebfbc5d4f..96bb9b5a4b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx @@ -1,5 +1,6 @@ -import { Layout, Menu } from "antd"; -import { usePathname } from "next/navigation"; +"use client"; + +import { Layout, Menu, ConfigProvider } from "antd"; import { KeyOutlined, PlayCircleOutlined, @@ -18,126 +19,191 @@ import { ExperimentOutlined, ToolOutlined, TagsOutlined, - BgColorsOutlined, } from "@ant-design/icons"; -import { ConfigProvider } from "antd"; -import useFeatureFlags from "@/hooks/useFeatureFlags"; +// import { +// all_admin_roles, +// rolesWithWriteAccess, +// internalUserRoles, +// isAdminRole, +// } from "../utils/roles"; +// import UsageIndicator from "./usage_indicator"; +import * as React from "react"; +import { useRouter, usePathname } from "next/navigation"; import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "@/utils/roles"; import UsageIndicator from "@/components/usage_indicator"; -import React from "react"; + const { Sider } = Layout; +// -------- Types -------- interface SidebarProps { accessToken: string | null; - setPage: (page: string) => void; userRole: string; + /** Fallback selection id (legacy), used if path can't be matched */ defaultSelectedKey: string; collapsed?: boolean; } -interface MenuItem { +interface MenuItemCfg { key: string; - page: string; + page: string; // legacy id; we map this to a path below label: string; roles?: string[]; - children?: MenuItem[]; + children?: MenuItemCfg[]; icon?: React.ReactNode; } -/** ---- BASE URL HELPERS ---- */ -function normalizeBasePrefix(raw: string | undefined | null): string { - const trimmed = (raw ?? "").trim(); - if (!trimmed) return ""; // no base - const core = trimmed.replace(/^\/+/, "").replace(/\/+$/, ""); - return core ? `/${core}` : ""; -} -const BASE_PREFIX = normalizeBasePrefix(process.env.NEXT_PUBLIC_BASE_URL); +/** ---------- 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 +}; -/** Build an absolute path under the configured base. */ -function withBase(path: string): string { - // path can be "/virtual-keys" or "/?page=..." - const p = path.startsWith("/") ? path : `/${path}`; - return `${BASE_PREFIX}${p}` || p; -} +/** Map legacy `page` ids to real app routes (relative, no leading slash). */ +const routeFor = (slug: string): string => { + switch (slug) { + // top level + case "api-keys": + return "virtual-keys"; + case "llm-playground": + return "test-key"; + case "models": + return "models-and-endpoints"; + case "new_usage": + return "usage"; + case "teams": + return "teams"; + case "organizations": + return "organizations"; + case "users": + return "users"; + case "api_ref": + return "api-reference"; + case "model-hub-table": + // If you intend the newer in-dashboard page, use "model-hub". + return "model-hub"; + case "logs": + return "logs"; + case "guardrails": + return "guardrails"; -/** History-based navigation to prevent hard reloads / suspense flashes */ -function softNavigate(url: string, replace = false) { - if (typeof window === "undefined") return; - if (replace) window.history.replaceState(null, "", url); - else window.history.pushState(null, "", url); -} -/** -------------------------------- */ + // tools + case "mcp-servers": + return "tools/mcp-servers"; + case "vector-stores": + return "tools/vector-stores"; -const Sidebar2: React.FC = ({ - accessToken, - setPage, - userRole, - defaultSelectedKey, - collapsed = false, -}) => { - const pathname = usePathname(); - const { refactoredUIFlag } = useFeatureFlags(); + // experimental + case "caching": + return "experimental/caching"; + case "prompts": + return "experimental/prompts"; + case "budgets": + return "experimental/budgets"; + case "transform-request": + return "experimental/api-playground"; + case "tag-management": + return "experimental/tag-management"; + case "usage": // "Old Usage" + return "experimental/old-usage"; - const menuItems: MenuItem[] = [ - { key: "1", page: "api-keys", label: "Virtual Keys", icon: }, + // settings + case "general-settings": + return "settings/router-settings"; + case "settings": // "Logging & Alerts" + return "settings/logging-and-alerts"; + case "admin-panel": + return "settings/admin-settings"; + case "ui-theme": + return "settings/ui-theme"; + + default: + // treat as already a relative path + return slug.replace(/^\/+/, ""); + } +}; + +/** Prefix base path ("/" or "/ui/") */ +const toHref = (slugOrPath: string) => { + const base = getBasePath(); // "/" or "/ui/" + const rel = routeFor(slugOrPath).replace(/^\/+|\/+$/g, ""); + return `${base}${rel}`; +}; + +const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => { + const router = useRouter(); + const pathname = usePathname() || "/"; + + // ----- Menu config (unchanged labels/icons; same appearance) ----- + const menuItems: MenuItemCfg[] = [ + { key: "1", page: "api-keys", label: "Virtual Keys", icon: }, { key: "3", page: "llm-playground", label: "Test Key", - icon: , + icon: , roles: rolesWithWriteAccess, }, { key: "2", page: "models", label: "Models + Endpoints", - icon: , + icon: , roles: rolesWithWriteAccess, }, { key: "12", page: "new_usage", label: "Usage", - icon: , + icon: , roles: [...all_admin_roles, ...internalUserRoles], }, - { key: "6", page: "teams", label: "Teams", icon: }, + { key: "6", page: "teams", label: "Teams", icon: }, { key: "17", page: "organizations", label: "Organizations", - icon: , + icon: , roles: all_admin_roles, }, { key: "5", page: "users", label: "Internal Users", - icon: , + icon: , roles: all_admin_roles, }, - { key: "14", page: "api_ref", label: "API Reference", icon: }, - { key: "16", page: "model-hub-table", label: "Model Hub", icon: }, - { key: "15", page: "logs", label: "Logs", icon: }, + { key: "14", page: "api_ref", label: "API Reference", icon: }, + { + key: "16", + page: "model-hub-table", + label: "Model Hub", + icon: , + }, + { key: "15", page: "logs", label: "Logs", icon: }, { key: "11", page: "guardrails", label: "Guardrails", - icon: , + icon: , roles: all_admin_roles, }, { key: "26", page: "tools", label: "Tools", - icon: , + icon: , children: [ - { key: "18", page: "mcp-servers", label: "MCP Servers", icon: }, + { key: "18", page: "mcp-servers", label: "MCP Servers", icon: }, { key: "21", page: "vector-stores", label: "Vector Stores", - icon: , + icon: , roles: all_admin_roles, }, ], @@ -146,136 +212,135 @@ const Sidebar2: React.FC = ({ key: "experimental", page: "experimental", label: "Experimental", - icon: , + icon: , children: [ { key: "9", page: "caching", label: "Caching", - icon: , + icon: , roles: all_admin_roles, }, { key: "25", page: "prompts", label: "Prompts", - icon: , + icon: , roles: all_admin_roles, }, { key: "10", page: "budgets", label: "Budgets", - icon: , + icon: , roles: all_admin_roles, }, { key: "20", page: "transform-request", label: "API Playground", - icon: , + icon: , roles: [...all_admin_roles, ...internalUserRoles], }, { key: "19", page: "tag-management", label: "Tag Management", - icon: , + icon: , roles: all_admin_roles, }, - { key: "4", page: "usage", label: "Old Usage", icon: }, + { key: "4", page: "usage", label: "Old Usage", icon: }, ], }, { key: "settings", page: "settings", label: "Settings", - icon: , + icon: , roles: all_admin_roles, children: [ { key: "11", page: "general-settings", label: "Router Settings", - icon: , + icon: , roles: all_admin_roles, }, { key: "8", page: "settings", label: "Logging & Alerts", - icon: , + icon: , roles: all_admin_roles, }, { key: "13", page: "admin-panel", label: "Admin Settings", - icon: , + icon: , roles: all_admin_roles, }, { key: "14", page: "ui-theme", label: "UI Theme", - icon: , + icon: , roles: all_admin_roles, }, ], }, ]; - const findMenuItemKey = (page: string): string => { - const topLevelItem = menuItems.find((item) => item.page === page); - if (topLevelItem) return topLevelItem.key; - for (const item of menuItems) { + // ----- Filter by role without mutating originals ----- + const filteredMenuItems = React.useMemo(() => { + return menuItems + .filter((item) => !item.roles || item.roles.includes(userRole)) + .map((item) => ({ + ...item, + children: item.children ? item.children.filter((c) => !c.roles || c.roles.includes(userRole)) : undefined, + })); + }, [userRole]); + + // ----- Compute selected key from current path ----- + const selectedMenuKey = React.useMemo(() => { + const base = getBasePath(); + // strip base prefix and leading slash -> "virtual-keys", "tools/mcp-servers", etc. + const rel = pathname.startsWith(base) ? pathname.slice(base.length) : pathname.replace(/^\/+/, ""); + const relLower = rel.toLowerCase(); + + const matchesPath = (slug: string) => { + const route = routeFor(slug).toLowerCase(); + return relLower === route || relLower.startsWith(`${route}/`); + }; + + // search top-level + for (const item of filteredMenuItems) { + if (!item.children && matchesPath(item.page)) return item.key; if (item.children) { - const childItem = item.children.find((child) => child.page === page); - if (childItem) return childItem.key; + for (const child of item.children) { + if (matchesPath(child.page)) return child.key; + } } } + + // fallback to legacy defaultSelectedKey mapping + const fallback = filteredMenuItems.find((i) => i.page === defaultSelectedKey)?.key; + if (fallback) return fallback; + + for (const item of filteredMenuItems) { + if (item.children?.some((c) => c.page === defaultSelectedKey)) { + const child = item.children.find((c) => c.page === defaultSelectedKey)!; + return child.key; + } + } + return "1"; - }; + }, [pathname, filteredMenuItems, defaultSelectedKey]); - const selectedMenuKey = findMenuItemKey(defaultSelectedKey); - - const filteredMenuItems = menuItems.filter((item) => { - const hasParentAccess = !item.roles || item.roles.includes(userRole); - if (!hasParentAccess) return false; - if (item.children) { - item.children = item.children.filter((child) => !child.roles || child.roles.includes(userRole)); - } - return true; - }); - - // Helper: update /?page=... under the configured base, WITHOUT triggering App Router nav - const pushToRootWithPage = (page: string, useReplace = false) => { - const params = new URLSearchParams(); - params.set("page", page); - const url = withBase(`/?${params.toString()}`); - softNavigate(url, useReplace); - }; - - const navigateToPage = (page: string) => { - if (page === "api-keys") { - if (refactoredUIFlag) { - // vanity URL, keep SPA alive - softNavigate(withBase("/virtual-keys")); - return; // don't call setPage to keep parity, UI already shows api-keys by default - } - pushToRootWithPage(page); - setPage(page); - return; - } - - if (refactoredUIFlag) { - const onVirtualKeys = - typeof window !== "undefined" && window.location.pathname.startsWith(withBase("/virtual-keys")); - pushToRootWithPage(page, onVirtualKeys); - } else { - pushToRootWithPage(page); - } - setPage(page); + // ----- Navigation ----- + const goTo = (slug: string) => { + const href = toHref(slug); + router.push(href); }; return ( @@ -287,16 +352,32 @@ const Sidebar2: React.FC = ({ collapsedWidth={80} collapsible trigger={null} - style={{ transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", position: "relative" }} + style={{ + transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", + position: "relative", + }} > - + ({ key: item.key, icon: item.icon, @@ -305,9 +386,9 @@ const Sidebar2: React.FC = ({ key: child.key, icon: child.icon, label: child.label, - onClick: () => navigateToPage(child.page), + onClick: () => goTo(child.page), })), - onClick: !item.children ? () => navigateToPage(item.page) : undefined, + onClick: !item.children ? () => goTo(item.page) : undefined, }))} /> diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx new file mode 100644 index 0000000000..cb7f0f1a32 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx @@ -0,0 +1,29 @@ +import useFeatureFlags from "@/hooks/useFeatureFlags"; +import Sidebar from "@/components/leftnav"; +import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +interface SidebarProviderProps { + defaultSelectedKey: string; + setPage: (newPage: string) => void; + sidebarCollapsed: boolean; +} + +const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: SidebarProviderProps) => { + const { refactoredUIFlag } = useFeatureFlags(); + const { accessToken, userRole } = useAuthorized(); + + return refactoredUIFlag ? ( + + ) : ( + + ); +}; + +export default SidebarProvider; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx index fce7f69192..9eb4191be1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx @@ -70,7 +70,7 @@ const CreateUserModal: React.FC = ({ possibleUIRoles, onUs const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false); const [invitationLinkData, setInvitationLinkData] = useState(null); const [baseUrl, setBaseUrl] = useState(null); - const teams = useTeams(); + const { teams } = useTeams(); // get all models useEffect(() => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/experimental/api-playground/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/experimental/api-playground/page.tsx new file mode 100644 index 0000000000..0948b7626d --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/experimental/api-playground/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import TransformRequestPanel from "@/components/transform_request"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const APIPlaygroundPage = () => { + const { accessToken } = useAuthorized(); + + return ; +}; + +export default APIPlaygroundPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/experimental/budgets/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/experimental/budgets/page.tsx new file mode 100644 index 0000000000..e49bd342c0 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/experimental/budgets/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import BudgetPanel from "@/components/budgets/budget_panel"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const BudgetsPage = () => { + const { accessToken } = useAuthorized(); + + return ; +}; + +export default BudgetsPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/experimental/caching/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/experimental/caching/page.tsx new file mode 100644 index 0000000000..6dcbcdc697 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/experimental/caching/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import CacheDashboard from "@/components/cache_dashboard"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const CachingPage = () => { + const { token, accessToken, userRole, userId, premiumUser } = useAuthorized(); + + return ( + + ); +}; + +export default CachingPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/experimental/old-usage/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/experimental/old-usage/page.tsx new file mode 100644 index 0000000000..9521f4f69f --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/experimental/old-usage/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import Usage from "@/components/usage"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useState } from "react"; + +const OldUsagePage = () => { + const { accessToken, token, userRole, userId, premiumUser } = useAuthorized(); + const [keys, setKeys] = useState([]); + + return ( + + ); +}; + +export default OldUsagePage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/experimental/prompts/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/experimental/prompts/page.tsx new file mode 100644 index 0000000000..0836a03b7e --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/experimental/prompts/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import PromptsPanel from "@/components/prompts"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const PromptsPage = () => { + const { accessToken } = useAuthorized(); + + return ; +}; + +export default PromptsPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/experimental/tag-management/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/experimental/tag-management/page.tsx new file mode 100644 index 0000000000..0e686387b3 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/experimental/tag-management/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import TagManagement from "@/components/tag_management"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const TagManagementPage = () => { + const { accessToken, userId, userRole } = useAuthorized(); + + return ; +}; + +export default TagManagementPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/page.tsx new file mode 100644 index 0000000000..50cee215eb --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import GuardrailsPanel from "@/components/guardrails"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const GuardrailsPage = () => { + const { accessToken } = useAuthorized(); + + return ; +}; + +export default GuardrailsPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts index 74c0476f19..ebb0c34592 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts @@ -1,9 +1,9 @@ "use client"; import { useEffect, useMemo } from "react"; -import { getCookie, clearTokenCookies } from "@/utils/cookieUtils"; import { useRouter } from "next/navigation"; import { jwtDecode } from "jwt-decode"; +import { clearTokenCookies, getCookie } from "@/utils/cookieUtils"; const useAuthorized = () => { const router = useRouter(); @@ -31,10 +31,13 @@ const useAuthorized = () => { }, [token, router]); return { + token: token, accessToken: decoded?.key ?? null, userId: decoded?.user_id ?? null, userRole: decoded?.user_role ?? null, premiumUser: decoded?.premium_user ?? null, + disabledPersonalKeyCreation: decoded?.disabled_non_admin_personal_key_creation ?? null, + showSSOBanner: decoded?.login_method === "username_password" ?? false, }; }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx new file mode 100644 index 0000000000..4c6e7acc19 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx @@ -0,0 +1,28 @@ +"use client"; + +import SpendLogsTable from "@/components/view_logs"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const LogsPage = () => { + const { accessToken, token, userRole, userId, premiumUser } = useAuthorized(); + const { teams } = useTeams(); + + const queryClient = new QueryClient(); + + return ( + + + + ); +}; + +export default LogsPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/model-hub/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/model-hub/page.tsx new file mode 100644 index 0000000000..86967b660f --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/model-hub/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import ModelHubTable from "@/components/model_hub_table"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const ModelHubPage = () => { + const { accessToken, premiumUser, userRole } = useAuthorized(); + + return ; +}; + +export default ModelHubPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx new file mode 100644 index 0000000000..2c523ea403 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import ModelDashboard from "@/components/templates/model_dashboard"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import { useState } from "react"; + +const ModelsAndEndpointsPage = () => { + const { token, accessToken, userRole, userId, premiumUser } = useAuthorized(); + const [keys, setKeys] = useState([]); + + const { teams } = useTeams(); + + return ( + {}} + premiumUser={premiumUser} + teams={teams} + /> + ); +}; + +export default ModelsAndEndpointsPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/page.tsx new file mode 100644 index 0000000000..6112fac316 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import Organizations, { fetchOrganizations } from "@/components/organizations"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useEffect, useState } from "react"; +import { Organization } from "@/components/networking"; +import { fetchUserModels } from "@/components/organisms/create_key_button"; + +const OrganizationsPage = () => { + const { userId: userID, accessToken, userRole, premiumUser } = useAuthorized(); + const [organizations, setOrganizations] = useState([]); + const [userModels, setUserModels] = useState([]); + + useEffect(() => { + fetchOrganizations(accessToken, setOrganizations).then(() => {}); + }, [accessToken]); + + useEffect(() => { + fetchUserModels(userID, userRole, accessToken, setUserModels).then(() => {}); + }, [userID, userRole, accessToken]); + + return ( + + ); +}; + +export default OrganizationsPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx new file mode 100644 index 0000000000..a851ce766a --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import AdminPanel from "@/components/admins"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useState } from "react"; +import { Team } from "@/components/key_team_helpers/key_list"; +import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; + +const AdminSettings = () => { + const { teams, setTeams } = useTeams(); + + const [searchParams, setSearchParams] = useState(() => + typeof window === "undefined" ? new URLSearchParams() : new URLSearchParams(window.location.search), + ); + const { accessToken, userId, premiumUser, showSSOBanner } = useAuthorized(); + + return ( + + ); +}; + +export default AdminSettings; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/settings/logging-and-alerts/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/settings/logging-and-alerts/page.tsx new file mode 100644 index 0000000000..b13e3c42f9 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/settings/logging-and-alerts/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import Settings from "@/components/settings"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const LoggingAndAlertsPage = () => { + const { accessToken, userRole, userId, premiumUser } = useAuthorized(); + + return ; +}; + +export default LoggingAndAlertsPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/settings/router-settings/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/settings/router-settings/page.tsx new file mode 100644 index 0000000000..2b5463cd81 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/settings/router-settings/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import GeneralSettings from "@/components/general_settings"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const RouterSettingsPage = () => { + const { accessToken, userRole, userId } = useAuthorized(); + + return ; +}; + +export default RouterSettingsPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/settings/ui-theme/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/settings/ui-theme/page.tsx new file mode 100644 index 0000000000..c6826cf11d --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/settings/ui-theme/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import UIThemeSettings from "@/components/ui_theme_settings"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const UIThemePage = () => { + const { userId, userRole, accessToken } = useAuthorized(); + + return ; +}; + +export default UIThemePage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx new file mode 100644 index 0000000000..f1f0460d53 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx @@ -0,0 +1,35 @@ +"use client"; + +import Teams from "@/components/teams"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import { useEffect, useState } from "react"; +import { Organization } from "@/components/networking"; +import { fetchOrganizations } from "@/components/organizations"; + +const TeamsPage = () => { + const { accessToken, userId, userRole } = useAuthorized(); + const { teams, setTeams } = useTeams(); + const [searchParams, setSearchParams] = useState(() => + typeof window === "undefined" ? new URLSearchParams() : new URLSearchParams(window.location.search), + ); + const [organizations, setOrganizations] = useState([]); + + useEffect(() => { + fetchOrganizations(accessToken, setOrganizations).then(() => {}); + }, [accessToken]); + + return ( + + ); +}; + +export default TeamsPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/test-key/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/test-key/page.tsx new file mode 100644 index 0000000000..6324f3c555 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/test-key/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import ChatUI from "@/components/chat_ui/ChatUI"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const TestKeyPage = () => { + const { token, accessToken, userRole, userId, disabledPersonalKeyCreation } = useAuthorized(); + + return ( + + ); +}; + +export default TestKeyPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tools/mcp-servers/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tools/mcp-servers/page.tsx new file mode 100644 index 0000000000..1bea7ac74a --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/tools/mcp-servers/page.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { MCPServers } from "@/components/mcp_tools"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const MCPServersPage = () => { + const { accessToken, userRole, userId } = useAuthorized(); + + const queryClient = new QueryClient(); + + return ( + + + + ); +}; + +export default MCPServersPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tools/vector-stores/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tools/vector-stores/page.tsx new file mode 100644 index 0000000000..8516a0faa1 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/tools/vector-stores/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import VectorStoreManagement from "@/components/vector_store_management"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const VectorStoresPage = () => { + const { accessToken, userId, userRole } = useAuthorized(); + + return ; +}; + +export default VectorStoresPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx new file mode 100644 index 0000000000..c20b13c67d --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx @@ -0,0 +1,22 @@ +"use client"; + +import NewUsagePage from "@/components/new_usage"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; + +const UsagePage = () => { + const { accessToken, userRole, userId, premiumUser } = useAuthorized(); + const { teams } = useTeams(); + + return ( + + ); +}; + +export default UsagePage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx new file mode 100644 index 0000000000..dd53166574 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx @@ -0,0 +1,31 @@ +"use client"; + +import ViewUserDashboard from "@/components/view_users"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import { useState } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const UsersPage = () => { + const { accessToken, userRole, userId, token } = useAuthorized(); + const [keys, setKeys] = useState([]); + + const { teams } = useTeams(); + const queryClient = new QueryClient(); + + return ( + + + + ); +}; + +export default UsersPage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx index 015d8c43f9..180d3336f1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx @@ -61,7 +61,7 @@ const VirtualKeysTable = ({ setAccessToken, }: AllKeysTableProps) => { const { userId: userID, userRole, accessToken, premiumUser } = useAuthorized(); - const teams = useTeams(); + const { teams } = useTeams(); const [selectedKeyId, setSelectedKeyId] = useState(null); const [userList, setUserList] = useState([]); const [sorting, setSorting] = React.useState(() => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx index 5f04f8a28a..7ba7a2fea5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx @@ -4,7 +4,7 @@ import { Team } from "@/components/key_team_helpers/key_list"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; const useTeams = () => { - const [teams, setTeams] = useState([]); + const [teams, setTeams] = useState([]); const { accessToken, userId: userID, userRole } = useAuthorized(); useEffect(() => { @@ -14,7 +14,7 @@ const useTeams = () => { })(); }, [accessToken, userID, userRole]); - return teams; + return { teams, setTeams }; }; export default useTeams; diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index f83972e0c1..ceee52399e 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { Suspense, useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; import { jwtDecode } from "jwt-decode"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Team } from "@/components/key_team_helpers/key_list"; @@ -22,6 +23,7 @@ import ModelHubTable from "@/components/model_hub_table"; import NewUsagePage from "@/components/new_usage"; import APIRef from "@/components/api_ref"; import ChatUI from "@/components/chat_ui/ChatUI"; +import Sidebar from "@/components/leftnav"; import Usage from "@/components/usage"; import CacheDashboard from "@/components/cache_dashboard"; import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking"; @@ -37,35 +39,50 @@ import VectorStoreManagement from "@/components/vector_store_management"; import UIThemeSettings from "@/components/ui_theme_settings"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { cx } from "@/lib/cva.config"; -import Sidebar2 from "@/app/(dashboard)/components/Sidebar2"; - -/** ---- BASE URL HELPERS ---- */ -function normalizeBasePrefix(raw: string | undefined | null): string { - const trimmed = (raw ?? "").trim(); - if (!trimmed) return ""; - const core = trimmed.replace(/^\/+/, "").replace(/\/+$/, ""); - return core ? `/${core}` : ""; -} -const BASE_PREFIX = normalizeBasePrefix(process.env.NEXT_PUBLIC_BASE_URL); -function withBase(path: string): string { - const p = path.startsWith("/") ? path : `/${path}`; - return `${BASE_PREFIX}${p}` || p; -} -/** -------------------------------- */ +import useFeatureFlags from "@/hooks/useFeatureFlags"; +import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider"; function getCookie(name: string) { - if (typeof document === "undefined") return null; - const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); - return cookieValue ? cookieValue.split("=")[1] : null; + // Safer cookie read + decoding; handles '=' inside values + const match = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); + if (!match) return null; + const value = match.slice(name.length + 1); + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +function deleteCookie(name: string, path = "/") { + // Best-effort client-side clear (works for non-HttpOnly cookies without Domain) + document.cookie = `${name}=; Max-Age=0; Path=${path}`; +} + +function isJwtExpired(token: string): boolean { + try { + const decoded: any = jwtDecode(token); + if (decoded && typeof decoded.exp === "number") { + return decoded.exp * 1000 <= Date.now(); + } + return false; + } catch { + // If we can't decode, treat as invalid/expired + return true; + } } function formatUserRole(userRole: string) { - if (!userRole) return "Undefined Role"; + if (!userRole) { + return "Undefined Role"; + } switch (userRole.toLowerCase()) { case "app_owner": + return "App Owner"; case "demo_app_owner": return "App Owner"; case "app_admin": + return "Admin"; case "proxy_admin": return "Admin"; case "proxy_admin_viewer": @@ -75,7 +92,7 @@ function formatUserRole(userRole: string) { case "internal_user": return "Internal User"; case "internal_user_viewer": - case "internal_viewer": + case "internal_viewer": // TODO:remove if deprecated return "Internal Viewer"; case "app_user": return "App User"; @@ -95,6 +112,7 @@ function LoadingScreen() { return (
🚅 LiteLLM
+
Loading... @@ -103,16 +121,6 @@ function LoadingScreen() { ); } -/** Derive the app "page" from the URL without triggering App Router navigations */ -function getPageFromLocation(loc: Location): string { - const sp = new URLSearchParams(loc.search); - const p = sp.get("page"); - if (p) return p; - // vanity route for refactored UI - if (loc.pathname.endsWith("/virtual-keys")) return "api-keys"; - return "api-keys"; -} - export default function CreateKeyPage() { const [userRole, setUserRole] = useState(""); const [premiumUser, setPremiumUser] = useState(false); @@ -122,98 +130,156 @@ export default function CreateKeyPage() { const [keys, setKeys] = useState([]); const [organizations, setOrganizations] = useState([]); const [userModels, setUserModels] = useState([]); - const [proxySettings, setProxySettings] = useState({ PROXY_BASE_URL: "", PROXY_LOGOUT_URL: "" }); + const [proxySettings, setProxySettings] = useState({ + PROXY_BASE_URL: "", + PROXY_LOGOUT_URL: "", + }); + const [showSSOBanner, setShowSSOBanner] = useState(true); - - // Stable local mirror of URLSearchParams (no suspense, no flashes) - const [searchParams, setSearchParams] = useState(() => - typeof window === "undefined" ? new URLSearchParams() : new URLSearchParams(window.location.search), - ); - const invitation_id = useMemo(() => searchParams.get("invitation_id"), [searchParams]); - - // Local page state (drives UI) - const [page, setPage] = useState(() => - typeof window === "undefined" ? "api-keys" : getPageFromLocation(window.location), - ); - - // Keep history/back/forward in sync with UI - useEffect(() => { - const onPop = () => { - if (typeof window === "undefined") return; - setSearchParams(new URLSearchParams(window.location.search)); - setPage(getPageFromLocation(window.location)); - }; - window.addEventListener("popstate", onPop); - return () => window.removeEventListener("popstate", onPop); - }, []); - - // Update URL without triggering App Router reload/suspense - const updatePage = (newPage: string) => { - if (typeof window === "undefined") return; - // 1) instant UI update - setPage(newPage); - // 2) URL update under base prefix - const sp = new URLSearchParams(window.location.search); - sp.set("page", newPage); - const url = withBase(`/?${sp.toString()}`); - window.history.pushState(null, "", url); - // 3) keep our local mirror in sync - setSearchParams(new URLSearchParams(sp)); - }; - + const searchParams = useSearchParams()!; + const [modelData, setModelData] = useState({ data: [] }); const [token, setToken] = useState(null); const [createClicked, setCreateClicked] = useState(false); const [authLoading, setAuthLoading] = useState(true); const [userID, setUserID] = useState(null); + const { refactoredUIFlag } = useFeatureFlags(); + + const invitation_id = searchParams.get("invitation_id"); + + // Get page from URL, default to 'api-keys' if not present + const [page, setPage] = useState(() => { + return searchParams.get("page") || "api-keys"; + }); + + // Custom setPage function that updates URL + const updatePage = (newPage: string) => { + // Update URL without full page reload + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("page", newPage); + + // Use Next.js router to update URL + window.history.pushState(null, "", `?${newSearchParams.toString()}`); + + setPage(newPage); + }; const [accessToken, setAccessToken] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - const toggleSidebar = () => setSidebarCollapsed((v) => !v); + const toggleSidebar = () => { + setSidebarCollapsed(!sidebarCollapsed); + }; const addKey = (data: any) => { setKeys((prevData) => (prevData ? [...prevData, data] : [data])); - setCreateClicked((v) => !v); + setCreateClicked(() => !createClicked); }; const redirectToLogin = authLoading === false && token === null && invitation_id === null; useEffect(() => { - const token = getCookie("token"); - getUiConfig().then(() => { - setToken(token); - setAuthLoading(false); - }); + let cancelled = false; + + (async () => { + try { + await getUiConfig(); // ensures proxyBaseUrl etc. are ready + } catch { + // proceed regardless; we still need to decide auth state + } + + if (cancelled) return; + + const raw = getCookie("token"); + const valid = raw && !isJwtExpired(raw) ? raw : null; + + // If token exists but is invalid/expired, clear it so downstream code + // doesn't keep trying to use it and cause redirect spasms. + if (raw && !valid) { + deleteCookie("token", "/"); + } + + if (!cancelled) { + setToken(valid); + setAuthLoading(false); + } + })(); + + return () => { + cancelled = true; + }; }, []); useEffect(() => { if (redirectToLogin) { - window.location.href = (proxyBaseUrl || "") + "/sso/key/generate"; + // Replace instead of assigning to avoid back-button loops + const dest = (proxyBaseUrl || "") + "/sso/key/generate"; + window.location.replace(dest); } }, [redirectToLogin]); useEffect(() => { - if (!token) return; - const decoded = jwtDecode(token) as { [key: string]: any }; - if (!decoded) return; - - setAccessToken(decoded.key); - setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation); - - if (decoded.user_role) { - const formattedUserRole = formatUserRole(decoded.user_role); - setUserRole(formattedUserRole); - if (formattedUserRole === "Admin Viewer") setPage("usage"); + if (!token) { + return; + } + + // Defensive: re-check expiry in case cookie changed after mount + if (isJwtExpired(token)) { + deleteCookie("token", "/"); + setToken(null); + return; + } + + let decoded: any = null; + try { + decoded = jwtDecode(token); + } catch { + // Malformed token → treat as unauthenticated + deleteCookie("token", "/"); + setToken(null); + return; + } + + if (decoded) { + // set accessToken + setAccessToken(decoded.key); + + setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation); + + // check if userRole is defined + if (decoded.user_role) { + const formattedUserRole = formatUserRole(decoded.user_role); + setUserRole(formattedUserRole); + if (formattedUserRole == "Admin Viewer") { + setPage("usage"); + } + } + + if (decoded.user_email) { + setUserEmail(decoded.user_email); + } + + if (decoded.login_method) { + setShowSSOBanner(decoded.login_method == "username_password" ? true : false); + } + + if (decoded.premium_user) { + setPremiumUser(decoded.premium_user); + } + + if (decoded.auth_header_name) { + setGlobalLitellmHeaderName(decoded.auth_header_name); + } + + if (decoded.user_id) { + setUserID(decoded.user_id); + } } - if (decoded.user_email) setUserEmail(decoded.user_email); - if (decoded.login_method) setShowSSOBanner(decoded.login_method === "username_password"); - if (decoded.premium_user) setPremiumUser(decoded.premium_user); - if (decoded.auth_header_name) setGlobalLitellmHeaderName(decoded.auth_header_name); - if (decoded.user_id) setUserID(decoded.user_id); }, [token]); useEffect(() => { if (accessToken && userID && userRole) { fetchUserModels(userID, userRole, accessToken, setUserModels); + } + if (accessToken && userID && userRole) { fetchTeams(accessToken, userID, userRole, null, setTeams); } if (accessToken) { @@ -226,194 +292,201 @@ export default function CreateKeyPage() { } return ( - - - {invitation_id ? ( - - ) : ( -
- }> + + + {invitation_id ? ( + -
-
- -
+ ) : ( +
+ +
+
+ +
- {page === "api-keys" ? ( - - ) : page === "models" ? ( - {}} - premiumUser={premiumUser} - teams={teams} - /> - ) : page === "llm-playground" ? ( - - ) : page === "users" ? ( - - ) : page === "teams" ? ( - - ) : page === "organizations" ? ( - - ) : page === "admin-panel" ? ( - - ) : page === "api_ref" ? ( - - ) : page === "settings" ? ( - - ) : page === "budgets" ? ( - - ) : page === "guardrails" ? ( - - ) : page === "prompts" ? ( - - ) : page === "transform-request" ? ( - - ) : page === "general-settings" ? ( - - ) : page === "ui-theme" ? ( - - ) : page === "model-hub-table" ? ( - - ) : page === "caching" ? ( - - ) : page === "pass-through-settings" ? ( - - ) : page === "logs" ? ( - - ) : page === "mcp-servers" ? ( - - ) : page === "tag-management" ? ( - - ) : page === "vector-stores" ? ( - - ) : page === "new_usage" ? ( - - ) : ( - - )} + {page == "api-keys" ? ( + + ) : page == "models" ? ( + + ) : page == "llm-playground" ? ( + + ) : page == "users" ? ( + + ) : page == "teams" ? ( + + ) : page == "organizations" ? ( + + ) : page == "admin-panel" ? ( + + ) : page == "api_ref" ? ( + + ) : page == "settings" ? ( + + ) : page == "budgets" ? ( + + ) : page == "guardrails" ? ( + + ) : page == "prompts" ? ( + + ) : page == "transform-request" ? ( + + ) : page == "general-settings" ? ( + + ) : page == "ui-theme" ? ( + + ) : page == "model-hub-table" ? ( + + ) : page == "caching" ? ( + + ) : page == "pass-through-settings" ? ( + + ) : page == "logs" ? ( + + ) : page == "mcp-servers" ? ( + + ) : page == "tag-management" ? ( + + ) : page == "vector-stores" ? ( + + ) : page == "new_usage" ? ( + + ) : ( + + )} +
-
- )} -
-
+ )} + + + ); } From 825a040524dfca7b343b7427d30df5a7a0ff5f30 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Wed, 8 Oct 2025 17:38:42 -0700 Subject: [PATCH 15/17] moved virtual-key-refactor to its own branch --- .../components/modals/CreateUserModal.tsx | 3 +- .../app/(dashboard)/hooks/useAuthorized.ts | 1 + .../{virtual-keys => }/hooks/useTeams.tsx | 2 +- .../src/app/(dashboard)/logs/page.tsx | 2 +- .../(dashboard)/models-and-endpoints/page.tsx | 2 +- .../{virtual-keys => }/networking.ts | 0 .../settings/admin-settings/page.tsx | 2 +- .../src/app/(dashboard)/teams/page.tsx | 2 +- .../src/app/(dashboard)/usage/page.tsx | 2 +- .../src/app/(dashboard)/users/page.tsx | 2 +- .../virtual-keys/components/CreateKey.tsx | 108 --- .../CreateKeyForm/KeyDetailsSection.tsx | 150 ---- .../CreateKeyForm/OptionalSettingsSection.tsx | 460 ------------ .../CreateKeyForm/OwnershipSection.tsx | 132 ---- .../CreateKeyModal/CreateKeyModal.tsx | 235 ------ .../components/CreateKeyModal/hooks/index.ts | 5 - .../hooks/useGuardrailsAndPrompts.ts | 24 - .../hooks/useMcpAccessGroups.ts | 21 - .../CreateKeyModal/hooks/useTeamModels.ts | 26 - .../CreateKeyModal/hooks/useUserModels.ts | 21 - .../CreateKeyModal/hooks/useUserSearch.ts | 41 - .../components/CreateKeyModal/networking.ts | 84 --- .../components/CreateKeyModal/types.ts | 17 - .../components/CreateKeyModal/utils.ts | 187 ----- .../virtual-keys/components/KeyInfoView.tsx | 699 ------------------ .../virtual-keys/components/SaveKeyModal.tsx | 60 -- .../VirtualKeysTable/VirtualKeysTable.tsx | 667 ----------------- .../VirtualKeysTable/hooks/useFilterLogic.ts | 189 ----- .../src/app/(dashboard)/virtual-keys/page.tsx | 41 +- .../components/templates/view_key_table.tsx | 9 +- .../src/components/user_dashboard.tsx | 5 +- .../src/components/view_users.tsx | 2 + 32 files changed, 37 insertions(+), 3164 deletions(-) rename ui/litellm-dashboard/src/app/(dashboard)/{virtual-keys => }/hooks/useTeams.tsx (88%) rename ui/litellm-dashboard/src/app/(dashboard)/{virtual-keys => }/networking.ts (100%) delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/index.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/types.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/SaveKeyModal.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx index 9eb4191be1..be3ea48bf2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx @@ -29,8 +29,7 @@ import { } from "@/components/networking"; import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; import BulkCreateUsersButton from "@/components/bulk_create_users_button"; -import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; // Helper function to generate UUID compatible across all environments diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts index ebb0c34592..435000f9c8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts @@ -34,6 +34,7 @@ const useAuthorized = () => { token: token, accessToken: decoded?.key ?? null, userId: decoded?.user_id ?? null, + userEmail: decoded?.user_email ?? null, userRole: decoded?.user_role ?? null, premiumUser: decoded?.premium_user ?? null, disabledPersonalKeyCreation: decoded?.disabled_non_admin_personal_key_creation ?? null, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useTeams.tsx similarity index 88% rename from ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/hooks/useTeams.tsx index 7ba7a2fea5..64cbf624f9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useTeams.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking"; import { Team } from "@/components/key_team_helpers/key_list"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { fetchTeams } from "@/app/(dashboard)/networking"; const useTeams = () => { const [teams, setTeams] = useState([]); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx index 4c6e7acc19..b04a12e230 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx @@ -2,7 +2,7 @@ import SpendLogsTable from "@/components/view_logs"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const LogsPage = () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx index 2c523ea403..b661e4de3e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx @@ -2,7 +2,7 @@ import ModelDashboard from "@/components/templates/model_dashboard"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; import { useState } from "react"; const ModelsAndEndpointsPage = () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/networking.ts b/ui/litellm-dashboard/src/app/(dashboard)/networking.ts similarity index 100% rename from ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/networking.ts rename to ui/litellm-dashboard/src/app/(dashboard)/networking.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx index a851ce766a..c99242f0c3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx @@ -4,7 +4,7 @@ import AdminPanel from "@/components/admins"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { useState } from "react"; import { Team } from "@/components/key_team_helpers/key_list"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; const AdminSettings = () => { const { teams, setTeams } = useTeams(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx index f1f0460d53..42e6d250ee 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx @@ -2,7 +2,7 @@ import Teams from "@/components/teams"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; import { useEffect, useState } from "react"; import { Organization } from "@/components/networking"; import { fetchOrganizations } from "@/components/organizations"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx index c20b13c67d..e4b44e5a45 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx @@ -2,7 +2,7 @@ import NewUsagePage from "@/components/new_usage"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; const UsagePage = () => { const { accessToken, userRole, userId, premiumUser } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx index dd53166574..7cf401873d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx @@ -2,7 +2,7 @@ import ViewUserDashboard from "@/components/view_users"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; import { useState } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx deleted file mode 100644 index 82be28e495..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; -import { Button } from "@tremor/react"; -import { Modal, Form } from "antd"; -import { getPossibleUserRoles, Organization } from "@/components/networking"; -import { rolesWithWriteAccess } from "@/utils/roles"; -import { Team } from "@/components/key_team_helpers/key_list"; -import CreateKeyModal from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal"; -import CreateUserModal from "@/app/(dashboard)/components/modals/CreateUserModal"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -interface CreateKeyProps { - team: Team | null; - userRole: string | null; - data: any[] | null; - addKey: (data: any) => void; -} - -interface User { - user_id: string; - user_email: string; - role?: string; -} - -interface UserOption { - label: string; - value: string; - user: User; -} -const CreateKey: React.FC = ({ team, userRole, data, addKey }) => { - const { accessToken } = useAuthorized(); - const [form] = Form.useForm(); - const [isModalVisible, setIsModalVisible] = useState(false); - const [isCreateUserModalVisible, setIsCreateUserModalVisible] = useState(false); - const [newlyCreatedUserId, setNewlyCreatedUserId] = useState(null); - const [possibleUIRoles, setPossibleUIRoles] = useState>>({}); - - useEffect(() => { - const fetchPossibleRoles = async () => { - try { - if (accessToken) { - // Check if roles are cached in session storage - const cachedRoles = sessionStorage.getItem("possibleUserRoles"); - if (cachedRoles) { - setPossibleUIRoles(JSON.parse(cachedRoles)); - } else { - const availableUserRoles = await getPossibleUserRoles(accessToken); - sessionStorage.setItem("possibleUserRoles", JSON.stringify(availableUserRoles)); - setPossibleUIRoles(availableUserRoles); - } - } - } catch (error) { - console.error("Error fetching possible user roles:", error); - } - }; - - fetchPossibleRoles(); - }, [accessToken]); - - // Add a callback function to handle user creation - const handleUserCreated = (userId: string) => { - setNewlyCreatedUserId(userId); - form.setFieldsValue({ user_id: userId }); - setIsCreateUserModalVisible(false); - }; - - const handleUserSelect = (_value: string, option: UserOption): void => { - const selectedUser = option.user; - form.setFieldsValue({ - user_id: selectedUser.user_id, - }); - }; - - return ( -
- {userRole && rolesWithWriteAccess.includes(userRole) && ( - - )} - - - {isCreateUserModalVisible && ( - setIsCreateUserModalVisible(false)} - footer={null} - width={800} - > - - - )} -
- ); -}; - -export default CreateKey; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx deleted file mode 100644 index 33a8ae06cf..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx +++ /dev/null @@ -1,150 +0,0 @@ -"use client"; - -import { TextInput, Title } from "@tremor/react"; -import { Form, FormInstance, Select, Tooltip } from "antd"; -import { InfoCircleOutlined } from "@ant-design/icons"; -import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; -import React from "react"; - -export interface KeyDetailsSectionProps { - form: FormInstance; - keyOwner: string; - modelsToPick: string[]; - keyType: string; - setKeyType: (keyType: string) => void; -} - -const { Option } = Select; - -const KeyDetailsSection = ({ form, keyOwner, keyType, modelsToPick, setKeyType }: KeyDetailsSectionProps) => { - return ( -
- Key Details - - {keyOwner === "you" || keyOwner === "another_user" ? "Key Name" : "Service Account ID"}{" "} - - - - - } - name="key_alias" - rules={[ - { - required: true, - message: `Please input a ${keyOwner === "you" ? "key name" : "service account ID"}`, - }, - ]} - help="required" - > - - - - - Models{" "} - - - - - } - name="models" - rules={ - keyType === "management" || keyType === "read_only" - ? [] - : [{ required: true, message: "Please select a model" }] - } - help={ - keyType === "management" || keyType === "read_only" - ? "Models field is disabled for this key type" - : "required" - } - className="mt-4" - > - - - - - Key Type{" "} - - - - - } - name="key_type" - initialValue="default" - className="mt-4" - > - - -
- ); -}; - -export default KeyDetailsSection; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx deleted file mode 100644 index b88f891944..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx +++ /dev/null @@ -1,460 +0,0 @@ -"use client"; - -import { Accordion, AccordionBody, AccordionHeader, Text, Title } from "@tremor/react"; -import { Form, FormInstance, Input, Select, Tooltip } from "antd"; -import { InfoCircleOutlined } from "@ant-design/icons"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; -import NumericalInput from "@/components/shared/numerical_input"; -import BudgetDurationDropdown from "@/components/common_components/budget_duration_dropdown"; -import RateLimitTypeFormItem from "@/components/common_components/RateLimitTypeFormItem"; -import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector"; -import MCPServerSelector from "@/components/mcp_server_management/MCPServerSelector"; -import PremiumLoggingSettings from "@/components/common_components/PremiumLoggingSettings"; -import ModelAliasManager from "@/components/common_components/ModelAliasManager"; -import KeyLifecycleSettings from "@/components/common_components/KeyLifecycleSettings"; -import { proxyBaseUrl } from "@/components/networking"; -import SchemaFormFields from "@/components/common_components/check_openapi_schema"; -import { Team } from "@/components/key_team_helpers/key_list"; -import React from "react"; -import { DefaultOptionType } from "rc-select/lib/Select"; -import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; - -export interface OptionalSettingsSectionProps { - form: FormInstance; - team: Team | null; - premiumUser: boolean; - guardrails: string[]; - prompts: string[]; - accessToken: string; - predefinedTags: DefaultOptionType[]; - loggingSettings: any; - setLoggingSettings: (settings: any) => void; - disabledCallbacks: string[]; - setDisabledCallbacks: (disabledCallbacks: string[]) => void; - modelAliases: ModelAliases; - setModelAliases: (aliases: ModelAliases) => void; - autoRotationEnabled: boolean; - setAutoRotationEnabled: (enabled: boolean) => void; - rotationInterval: string; - setRotationInterval: (interval: string) => void; -} - -// TODO: break apart this component into smaller sections -const OptionalSettingsSection = ({ - form, - team, - premiumUser, - guardrails, - prompts, - accessToken, - predefinedTags, - loggingSettings, - setLoggingSettings, - disabledCallbacks, - setDisabledCallbacks, - modelAliases, - setModelAliases, - autoRotationEnabled, - setAutoRotationEnabled, - rotationInterval, - setRotationInterval, -}: OptionalSettingsSectionProps) => { - return ( -
- - - Optional Settings - - - - Max Budget (USD){" "} - - - - - } - name="max_budget" - help={`Budget cannot exceed team max budget: $${team?.max_budget !== null && team?.max_budget !== undefined ? team?.max_budget : "unlimited"}`} - rules={[ - { - validator: async (_, value) => { - if (value && team && team.max_budget !== null && value > team.max_budget) { - throw new Error( - `Budget cannot exceed team max budget: $${formatNumberWithCommas(team.max_budget, 4)}`, - ); - } - }, - }, - ]} - > - - - - Reset Budget{" "} - - - - - } - name="budget_duration" - help={`Team Reset Budget: ${team?.budget_duration !== null && team?.budget_duration !== undefined ? team?.budget_duration : "None"}`} - > - form.setFieldValue("budget_duration", value)} /> - - - Tokens per minute Limit (TPM){" "} - - - - - } - name="tpm_limit" - help={`TPM cannot exceed team TPM limit: ${team?.tpm_limit !== null && team?.tpm_limit !== undefined ? team?.tpm_limit : "unlimited"}`} - rules={[ - { - validator: async (_, value) => { - if (value && team && team.tpm_limit !== null && value > team.tpm_limit) { - throw new Error(`TPM limit cannot exceed team TPM limit: ${team.tpm_limit}`); - } - }, - }, - ]} - > - - - - - Requests per minute Limit (RPM){" "} - - - - - } - name="rpm_limit" - help={`RPM cannot exceed team RPM limit: ${team?.rpm_limit !== null && team?.rpm_limit !== undefined ? team?.rpm_limit : "unlimited"}`} - rules={[ - { - validator: async (_, value) => { - if (value && team && team.rpm_limit !== null && value > team.rpm_limit) { - throw new Error(`RPM limit cannot exceed team RPM limit: ${team.rpm_limit}`); - } - }, - }, - ]} - > - - - - - Guardrails{" "} - - e.stopPropagation()} // Prevent accordion from collapsing when clicking link - > - - - - - } - name="guardrails" - className="mt-4" - help={ - premiumUser - ? "Select existing guardrails or enter new ones" - : "Premium feature - Upgrade to set guardrails by key" - } - > - ({ value: name, label: name }))} - /> - - - Allowed Vector Stores{" "} - - - - - } - name="allowed_vector_store_ids" - className="mt-4" - help="Select vector stores this key can access. Leave empty for access to all vector stores" - > - form.setFieldValue("allowed_vector_store_ids", values)} - value={form.getFieldValue("allowed_vector_store_ids")} - accessToken={accessToken} - placeholder="Select vector stores (optional)" - /> - - - - Allowed MCP Servers{" "} - - - - - } - name="allowed_mcp_servers_and_groups" - className="mt-4" - help="Select MCP servers or access groups this key can access. " - > - form.setFieldValue("allowed_mcp_servers_and_groups", val)} - value={form.getFieldValue("allowed_mcp_servers_and_groups")} - accessToken={accessToken} - placeholder="Select MCP servers or access groups (optional)" - /> - - - - Metadata{" "} - - - - - } - name="metadata" - className="mt-4" - > - - - - Tags{" "} - - - - - } - name="tags" - className="mt-4" - help={`Tags for tracking spend and/or doing tag-based routing.`} - > - handleUserSelect(value, option as UserOption)} - options={userOptions} - loading={userSearchLoading} - allowClear - style={{ width: "100%" }} - notFoundContent={userSearchLoading ? "Searching..." : "No users found"} - /> - setIsCreateUserModalVisible(true)} style={{ marginLeft: "8px" }}> - Create User - -
-
Search by email to find users
-
- - )} - - Team{" "} - - - - - } - name="team_id" - initialValue={team ? team.team_id : null} - className="mt-4" - rules={[ - { - required: keyOwner === "service_account", - message: "Please select a team for the service account", - }, - ]} - help={keyOwner === "service_account" ? "required" : ""} - > - { - const selectedTeam = teams?.find((t) => t.team_id === teamId) || null; - setSelectedCreateKeyTeam(selectedTeam); - }} - /> - -
- ); -}; - -export default OwnershipSection; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx deleted file mode 100644 index cbe3db4e8f..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx +++ /dev/null @@ -1,235 +0,0 @@ -"use client"; - -import { Button as Button2, Form, FormInstance, Modal } from "antd"; -import { Text } from "@tremor/react"; -import { keyCreateCall, keyCreateServiceAccountCall } from "@/components/networking"; -import React, { useEffect, useState } from "react"; -import { Team } from "@/components/key_team_helpers/key_list"; -import SaveKeyModal from "@/app/(dashboard)/virtual-keys/components/SaveKeyModal"; -import NotificationsManager from "@/components/molecules/notifications_manager"; -import { - useGuardrailsAndPrompts, - useMcpAccessGroups, - useTeamModels, - useUserModels, - useUserSearch, -} from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks"; -import OwnershipSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection"; -import KeyDetailsSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection"; -import OptionalSettingsSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection"; -import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; -import { getPredefinedTags, prepareFormValues } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils"; -import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export interface CreateKeyModalProps { - isModalVisible: boolean; - setIsModalVisible: (isModalVisible: boolean) => void; - form: FormInstance; - handleUserSelect: (_value: string, option: UserOption) => void; - setIsCreateUserModalVisible: (visible: boolean) => void; - team: Team | null; - data: any[] | null; - addKey: (data: any) => void; -} - -interface User { - user_id: string; - user_email: string; - role?: string; -} - -interface UserOption { - label: string; - value: string; - user: User; -} - -const CreateKeyModal = ({ - isModalVisible, - setIsModalVisible, - form, - handleUserSelect, - setIsCreateUserModalVisible, - team, - data, - addKey, -}: CreateKeyModalProps) => { - const { userId: userID, userRole, accessToken, premiumUser } = useAuthorized(); - const [apiKey, setApiKey] = useState(null); - const [keyOwner, setKeyOwner] = useState("you"); - const [keyType, setKeyType] = useState("default"); - - const [selectedCreateKeyTeam, setSelectedCreateKeyTeam] = useState(team); - - const [softBudget, setSoftBudget] = useState(null); - const [loggingSettings, setLoggingSettings] = useState([]); - const [disabledCallbacks, setDisabledCallbacks] = useState([]); - const [autoRotationEnabled, setAutoRotationEnabled] = useState(false); - const [rotationInterval, setRotationInterval] = useState("30d"); - const [modelAliases, setModelAliases] = useState({}); - const predefinedTags = getPredefinedTags(data); - - const { options: userOptions, loading: userSearchLoading, onSearch: handleUserSearch } = useUserSearch(); - const { guardrails, prompts } = useGuardrailsAndPrompts(); - const teams = useTeams(); - const mcpAccessGroups = useMcpAccessGroups(); - const userModels = useUserModels(); - const modelsToPick = useTeamModels(selectedCreateKeyTeam); - - const isTeamSelectionRequired = modelsToPick.includes("no-default-models"); - const isFormDisabled = isTeamSelectionRequired && !selectedCreateKeyTeam; - - const handleCancel = () => { - setIsModalVisible(false); - setApiKey(null); - setSelectedCreateKeyTeam(null); - form.resetFields(); - setLoggingSettings([]); - setDisabledCallbacks([]); - setKeyType("default"); - setModelAliases({}); - setAutoRotationEnabled(false); - setRotationInterval("30d"); - }; - - const handleOk = () => { - setIsModalVisible(false); - form.resetFields(); - setLoggingSettings([]); - setDisabledCallbacks([]); - setKeyType("default"); - setModelAliases({}); - setAutoRotationEnabled(false); - setRotationInterval("30d"); - }; - - const handleCreate = async (formValues: Record) => { - try { - const newKeyAlias = formValues?.key_alias ?? ""; - const newKeyTeamId = formValues?.team_id ?? null; - - const existingKeyAliases = data?.filter((k) => k.team_id === newKeyTeamId).map((k) => k.key_alias) ?? []; - - if (existingKeyAliases.includes(newKeyAlias)) { - throw new Error( - `Key alias ${newKeyAlias} already exists for team with ID ${newKeyTeamId}, please provide another key alias`, - ); - } - - NotificationsManager.info("Making API Call"); - setIsModalVisible(true); - - const prepared = prepareFormValues(formValues, { - keyOwner, - userID, - loggingSettings, - disabledCallbacks, - autoRotationEnabled, - rotationInterval, - modelAliases, - }); - - let response; - if (keyOwner === "service_account") { - response = await keyCreateServiceAccountCall(accessToken, prepared); - } else { - response = await keyCreateCall(accessToken, userID, prepared); - } - - // TODO: change logic to trigger API call in parent component - addKey(response); - - setApiKey(response["key"]); - setSoftBudget(response["soft_budget"]); - NotificationsManager.success("API Key Created"); - form.resetFields(); - localStorage.removeItem("userData" + userID); - } catch (error) { - console.log("error in create key:", error); - NotificationsManager.fromBackend(`Error creating the key: ${error}`); - } - }; - - useEffect(() => { - form.setFieldValue("models", []); - }, [selectedCreateKeyTeam, accessToken, userID, userRole, form]); - - return ( -
- -
- - - {/* Show message when team selection is required */} - {isFormDisabled && ( -
- - Please select a team to continue configuring your API key. If you do not see any teams, please contact - your Proxy Admin to either provide you with access to models or to add you to a team. - -
- )} - - {/* Section 2: Key Details */} - {!isFormDisabled && ( - - )} - - {/* Section 3: Optional Settings */} - {!isFormDisabled && ( - - )} - -
- - Create Key - -
- -
- {apiKey && ( - - )} -
- ); -}; - -export default CreateKeyModal; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/index.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/index.ts deleted file mode 100644 index ca3c4a663b..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./useGuardrailsAndPrompts"; -export * from "./useMcpAccessGroups"; -export * from "./useUserModels"; -export * from "./useTeamModels"; -export * from "./useUserSearch"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts deleted file mode 100644 index f4718e8ba0..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect, useState } from "react"; -import { fetchGuardrails, fetchPrompts } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export const useGuardrailsAndPrompts = () => { - const [guardrails, setGuardrails] = useState([]); - const [prompts, setPrompts] = useState([]); - const { accessToken } = useAuthorized(); - - useEffect(() => { - if (!accessToken) { - setGuardrails([]); - setPrompts([]); - return; - } - (async () => { - const [g, p] = await Promise.all([fetchGuardrails(accessToken), fetchPrompts(accessToken)]); - setGuardrails(g || []); - setPrompts(p || []); - })(); - }, [accessToken]); - - return { guardrails, prompts }; -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts deleted file mode 100644 index 998b392f77..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect, useState } from "react"; -import { getMCPAccessGroups } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export const useMcpAccessGroups = () => { - const [mcpAccessGroups, setMcpAccessGroups] = useState([]); - const { accessToken } = useAuthorized(); - - useEffect(() => { - if (!accessToken) { - setMcpAccessGroups([]); - return; - } - (async () => { - const groups = await getMCPAccessGroups(accessToken); - setMcpAccessGroups(groups || []); - })(); - }, [accessToken]); - - return mcpAccessGroups; -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts deleted file mode 100644 index dd99de0615..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import type { Team } from "@/components/key_team_helpers/key_list"; -import { fetchTeamModels } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export const useTeamModels = (selectedTeam: Team | null) => { - const { userId: userID, userRole, accessToken } = useAuthorized(); - const [modelsToPick, setModelsToPick] = useState([]); - - // turn array into a stable dep so it only re-runs when contents change - const selectedTeamModelsKey = useMemo(() => (selectedTeam?.models ?? []).join("|"), [selectedTeam?.models]); - - useEffect(() => { - if (!userID || !userRole || !accessToken) { - setModelsToPick([]); - return; - } - (async () => { - const fetched = await fetchTeamModels(userID, userRole, accessToken, selectedTeam?.team_id ?? null); - const union = Array.from(new Set([...(selectedTeam?.models ?? []), ...(fetched || [])])); - setModelsToPick(union); - })(); - }, [userID, userRole, accessToken, selectedTeam?.team_id, selectedTeamModelsKey, selectedTeam?.models]); - - return modelsToPick; -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts deleted file mode 100644 index 66b5520f06..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect, useState } from "react"; -import { getUserModelNames } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export const useUserModels = () => { - const { userId: userID, userRole, accessToken } = useAuthorized(); - const [userModels, setUserModels] = useState([]); - - useEffect(() => { - if (!userID || !userRole || !accessToken) { - setUserModels([]); - return; - } - (async () => { - const modelNames = await getUserModelNames(userID, userRole, accessToken); - setUserModels(modelNames || []); - })(); - }, [userID, userRole, accessToken]); - - return userModels; -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts deleted file mode 100644 index d266a51667..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { debounce } from "lodash"; -import { searchUserOptionsByEmail } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -type User = { user_id: string; user_email: string; role?: string }; -export interface UserOption { - label: string; - value: string; - user: User; -} - -export const useUserSearch = (delay = 300) => { - const { accessToken } = useAuthorized(); - const [options, setOptions] = useState([]); - const [loading, setLoading] = useState(false); - - const doSearch = async (raw: string) => { - const q = raw?.trim(); - if (!q || !accessToken) { - setOptions([]); - setLoading(false); - return; - } - setLoading(true); - try { - const res = await searchUserOptionsByEmail(accessToken, q); - setOptions(res || []); - } finally { - setLoading(false); - } - }; - - const onSearch = useMemo(() => debounce(doSearch, delay), [accessToken, delay]); - - useEffect(() => { - return () => onSearch.cancel(); - }, [onSearch]); - - return { options, loading, onSearch }; -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts deleted file mode 100644 index 377f6d3113..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - fetchMCPAccessGroups, - getGuardrailsList, - getPromptsList, - modelAvailableCall, - User, - userFilterUICall, -} from "@/components/networking"; -import { ModelAvailableResponse, UserOption } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; - -export const fetchGuardrails = async (accessToken: string) => { - try { - const response = await getGuardrailsList(accessToken); - return response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name); - } catch (error) { - console.error("Failed to fetch guardrails:", error); - return []; - } -}; - -export const fetchPrompts = async (accessToken: string) => { - try { - const response = await getPromptsList(accessToken); - return response.prompts.map((prompt) => prompt.prompt_id); - } catch (error) { - console.error("Failed to fetch prompts:", error); - return []; - } -}; - -export const searchUserOptionsByEmail = async (accessToken: string, emailQuery: string): Promise => { - const params = new URLSearchParams(); - params.append("user_email", emailQuery); - - const response = await userFilterUICall(accessToken, params); - const users: User[] = response; - - return users.map((user) => ({ - label: `${user.user_email} (${user.user_id})`, - value: user.user_id, - user, - })); -}; - -export const getUserModelNames = async (userID: string, userRole: string, accessToken: string): Promise => { - const res: ModelAvailableResponse = await modelAvailableCall(accessToken, userID, userRole); - return (res?.data ?? []).map((m) => m.id); -}; - -export const fetchTeamModels = async ( - userID: string, - userRole: string, - accessToken: string, - teamID: string | null, -): Promise => { - try { - if (userID === null || userRole === null) { - return []; - } - - if (accessToken !== null) { - const model_available = await modelAvailableCall(accessToken, userID, userRole, true, teamID, true); - let available_model_names = model_available["data"].map((element: { id: string }) => element.id); - console.log("available_model_names:", available_model_names); - return available_model_names; - } - return []; - } catch (error) { - console.error("Error fetching user models:", error); - return []; - } -}; - -export const getMCPAccessGroups = async (accessToken: string): Promise => { - try { - if (accessToken == null) { - return []; - } - return await fetchMCPAccessGroups(accessToken); - } catch (error) { - console.error("Failed to fetch MCP access groups:", error); - return []; - } -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/types.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/types.ts deleted file mode 100644 index 4d48df82d4..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface User { - user_id: string; - user_email: string; - role?: string; -} - -export interface UserOption { - label: string; - value: string; - user: User; -} - -export type Model = { id: string }; - -export type ModelAvailableResponse = { data: Model[] }; - -export type ModelAliases = { [key: string]: string }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts deleted file mode 100644 index 1fb40b8789..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { mapDisplayToInternalNames } from "@/components/callback_info_helpers"; -import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; - -export const getPredefinedTags = (data: any[] | null) => { - let allTags = []; - - console.log("data:", JSON.stringify(data)); - - if (data) { - for (let key of data) { - if (key["metadata"] && key["metadata"]["tags"]) { - allTags.push(...key["metadata"]["tags"]); - } - } - } - - const uniqueTags = Array.from(new Set(allTags)).map((tag) => ({ - value: tag, - label: tag, - })); - - console.log("uniqueTags:", uniqueTags); - return uniqueTags; -}; - -type AnyObj = Record; - -type PrepareOptions = { - keyOwner: string; - userID: string; - loggingSettings: any[]; - disabledCallbacks: string[]; - autoRotationEnabled: boolean; - rotationInterval: string; - modelAliases: ModelAliases; -}; - -export function safeParseMetadata(metadataStr: string | undefined): AnyObj { - try { - return JSON.parse(metadataStr || "{}"); - } catch (error) { - console.error("Error parsing metadata:", error); - return {}; - } -} - -export function withOwnerAssignments( - values: AnyObj, - { keyOwner, userID }: Pick, -): AnyObj { - // If owned by "you", set user_id - const base = keyOwner === "you" ? { ...values, user_id: userID } : { ...values }; - return base; -} - -export function enrichMetadata( - metadata: AnyObj, - values: AnyObj, - { - keyOwner, - loggingSettings, - disabledCallbacks, - }: Pick, -): AnyObj { - let next = { ...metadata }; - - // If it's a service account, add the service_account_id to the metadata - if (keyOwner === "service_account") { - next = { ...next, service_account_id: values.key_alias }; - } - - // Add logging settings to the metadata - if (Array.isArray(loggingSettings) && loggingSettings.length > 0) { - next = { ...next, logging: loggingSettings.filter((config: any) => config?.callback_name) }; - } - - // Add disabled callbacks to the metadata - if (Array.isArray(disabledCallbacks) && disabledCallbacks.length > 0) { - const mappedDisabledCallbacks = mapDisplayToInternalNames(disabledCallbacks); - next = { ...next, litellm_disabled_callbacks: mappedDisabledCallbacks }; - } - - return next; -} - -export function withAutoRotation( - values: AnyObj, - { autoRotationEnabled, rotationInterval }: Pick, -): AnyObj { - if (!autoRotationEnabled) return { ...values }; - return { ...values, auto_rotate: true, rotation_interval: rotationInterval }; -} - -export function withDurationNoop(values: AnyObj): AnyObj { - // Preserve the original no-op logic: if duration exists, set it to itself - if (values.duration) { - return { ...values, duration: values.duration }; - } - return { ...values }; -} - -export function mergeObjectPermission(values: AnyObj, patch: AnyObj): AnyObj { - const object_permission = { ...(values.object_permission || {}), ...patch }; - return { ...values, object_permission }; -} - -export function withVectorStores(values: AnyObj): AnyObj { - const { allowed_vector_store_ids, ...rest } = values; - if (Array.isArray(allowed_vector_store_ids) && allowed_vector_store_ids.length > 0) { - const patched = mergeObjectPermission(rest, { vector_stores: allowed_vector_store_ids }); - return patched; // original field removed by destructuring - } - return { ...values }; -} - -export function withMcpServersAndGroups(values: AnyObj): AnyObj { - const { allowed_mcp_servers_and_groups, ...rest } = values; - const servers = allowed_mcp_servers_and_groups?.servers; - const accessGroups = allowed_mcp_servers_and_groups?.accessGroups; - - const hasServers = Array.isArray(servers) && servers.length > 0; - const hasAccessGroups = Array.isArray(accessGroups) && accessGroups.length > 0; - - if (!hasServers && !hasAccessGroups) return { ...values }; - - let patched = { ...rest }; - if (hasServers) patched = mergeObjectPermission(patched, { mcp_servers: servers }); - if (hasAccessGroups) patched = mergeObjectPermission(patched, { mcp_access_groups: accessGroups }); - - // original field removed by destructuring - return patched; -} - -export function withMcpAccessGroups(values: AnyObj): AnyObj { - const { allowed_mcp_access_groups, ...rest } = values; - if (Array.isArray(allowed_mcp_access_groups) && allowed_mcp_access_groups.length > 0) { - const patched = mergeObjectPermission(rest, { mcp_access_groups: allowed_mcp_access_groups }); - return patched; // original field removed by destructuring - } - return { ...values }; -} - -function withAliases(values: AnyObj, modelAliases: ModelAliases): AnyObj { - if (modelAliases && Object.keys(modelAliases).length > 0) { - return { ...values, aliases: JSON.stringify(modelAliases) }; - } - return { ...values }; -} - -function withMetadataString(values: AnyObj, metadata: AnyObj): AnyObj { - return { ...values, metadata: JSON.stringify(metadata) }; -} - -export function prepareFormValues(initialValues: AnyObj, opts: PrepareOptions): AnyObj { - // Step 1: assign user id if needed - let values = withOwnerAssignments(initialValues, { keyOwner: opts.keyOwner, userID: opts.userID }); - - // Step 2: metadata parse & enrichment - const parsedMetadata = safeParseMetadata(values.metadata); - const enrichedMetadata = enrichMetadata(parsedMetadata, values, { - keyOwner: opts.keyOwner, - loggingSettings: opts.loggingSettings, - disabledCallbacks: opts.disabledCallbacks, - }); - - // Step 3: auto-rotation - values = withAutoRotation(values, { - autoRotationEnabled: opts.autoRotationEnabled, - rotationInterval: opts.rotationInterval, - }); - - // Step 4: duration (no-op preservation) - values = withDurationNoop(values); - - // Step 5: commit metadata string - values = withMetadataString(values, enrichedMetadata); - - // Step 6: object_permission transformations - values = withVectorStores(values); - values = withMcpServersAndGroups(values); - values = withMcpAccessGroups(values); - - // Step 7: aliases - values = withAliases(values, opts.modelAliases); - - return values; -} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx deleted file mode 100644 index 9f9a47fbe1..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx +++ /dev/null @@ -1,699 +0,0 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import { - Card, - Text, - Button, - Grid, - Tab, - TabList, - TabGroup, - TabPanel, - TabPanels, - Title, - Badge, -} from "@tremor/react"; -import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline"; -import { Form, Tooltip, Button as AntdButton } from "antd"; -import { copyToClipboard as utilCopyToClipboard, formatNumberWithCommas } from "@/utils/dataUtils"; -import { CopyIcon, CheckIcon } from "lucide-react"; -import { KeyResponse } from "@/components/key_team_helpers/key_list" -import { mapDisplayToInternalNames, mapInternalToDisplayNames } from "@/components/callback_info_helpers" -import NotificationManager from "@/components/molecules/notifications_manager" -import { keyDeleteCall, keyUpdateCall } from "@/components/networking" -import { parseErrorMessage } from "@/components/shared/errorUtils" -import { rolesWithWriteAccess } from "@/utils/roles" -import { RegenerateKeyModal } from "@/components/organisms/regenerate_key_modal" -import ObjectPermissionsView from "@/components/object_permissions_view" -import LoggingSettingsView from "@/components/logging_settings_view" -import { extractLoggingSettings, formatMetadataForDisplay } from "@/components/key_info_utils" -import AutoRotationView from "@/components/common_components/AutoRotationView" -import { KeyEditView } from "@/components/templates/key_edit_view" - - -interface KeyInfoViewProps { - keyId: string; - onClose: () => void; - keyData: KeyResponse | undefined; - onKeyDataUpdate?: (data: Partial) => void; - onDelete?: () => void; - accessToken: string | null; - userID: string | null; - userRole: string | null; - teams: any[] | null; - premiumUser: boolean; - setAccessToken?: (token: string) => void; - backButtonText?: string; -} - -const KeyInfoView = ({ - keyId, - onClose, - keyData, - accessToken, - userID, - userRole, - teams, - onKeyDataUpdate, - onDelete, - premiumUser, - setAccessToken, - backButtonText = "Back to Keys", - }: KeyInfoViewProps) => { - const [isEditing, setIsEditing] = useState(false); - const [form] = Form.useForm(); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [deleteConfirmInput, setDeleteConfirmInput] = useState(""); - const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false); - const [copiedStates, setCopiedStates] = useState>({}); - - // Add local state to maintain key data and track regeneration - const [currentKeyData, setCurrentKeyData] = useState(keyData); - const [lastRegeneratedAt, setLastRegeneratedAt] = useState(null); - const [isRecentlyRegenerated, setIsRecentlyRegenerated] = useState(false); - - // Update local state when keyData prop changes (but don't reset to undefined) - useEffect(() => { - if (keyData) { - setCurrentKeyData(keyData); - } - }, [keyData]); - - // Reset recent regeneration indicator after 5 seconds - useEffect(() => { - if (isRecentlyRegenerated) { - const timer = setTimeout(() => { - setIsRecentlyRegenerated(false); - }, 5000); - return () => clearTimeout(timer); - } - }, [isRecentlyRegenerated]); - - // Use currentKeyData instead of keyData throughout the component - if (!currentKeyData) { - return ( -
- - Key not found -
- ); - } - - const handleKeyUpdate = async (formValues: Record) => { - try { - if (!accessToken) return; - - const currentKey = formValues.token; - formValues.key = currentKey; - - // Guard premium features - if (!premiumUser) { - delete formValues.guardrails; - delete formValues.prompts; - } - - // Handle object_permission updates - if (formValues.vector_stores !== undefined) { - formValues.object_permission = { - ...currentKeyData.object_permission, - vector_stores: formValues.vector_stores || [], - }; - // Remove vector_stores from the top level as it should be in object_permission - delete formValues.vector_stores; - } - - if (formValues.mcp_servers_and_groups !== undefined) { - const { servers, accessGroups } = formValues.mcp_servers_and_groups || { servers: [], accessGroups: [] }; - formValues.object_permission = { - ...currentKeyData.object_permission, - mcp_servers: servers || [], - mcp_access_groups: accessGroups || [], - }; - // Remove mcp_servers_and_groups from the top level as it should be in object_permission - delete formValues.mcp_servers_and_groups; - } - - // Convert metadata back to an object if it exists and is a string - if (formValues.metadata && typeof formValues.metadata === "string") { - try { - const parsedMetadata = JSON.parse(formValues.metadata); - formValues.metadata = { - ...parsedMetadata, - ...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}), - ...(formValues.logging_settings ? { logging: formValues.logging_settings } : {}), - ...(formValues.disabled_callbacks?.length > 0 - ? { - litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks), - } - : {}), - }; - } catch (error) { - console.error("Error parsing metadata JSON:", error); - NotificationManager.error("Invalid metadata JSON"); - return; - } - } else { - formValues.metadata = { - ...(formValues.metadata || {}), - ...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}), - ...(formValues.logging_settings ? { logging: formValues.logging_settings } : {}), - ...(formValues.disabled_callbacks?.length > 0 - ? { - litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks), - } - : {}), - }; - } - - delete formValues.logging_settings; - - // Convert budget_duration to API format - if (formValues.budget_duration) { - const durationMap: Record = { - daily: "24h", - weekly: "7d", - monthly: "30d", - }; - formValues.budget_duration = durationMap[formValues.budget_duration]; - } - - const newKeyValues = await keyUpdateCall(accessToken, formValues); - - // Update local state - setCurrentKeyData((prevData) => (prevData ? { ...prevData, ...newKeyValues } : undefined)); - - if (onKeyDataUpdate) { - onKeyDataUpdate(newKeyValues); - } - NotificationManager.success("Key updated successfully"); - setIsEditing(false); - // Refresh key data here if needed - } catch (error) { - NotificationManager.fromBackend(parseErrorMessage(error)); - console.error("Error updating key:", error); - } - }; - - const handleDelete = async () => { - try { - if (!accessToken) return; - await keyDeleteCall(accessToken as string, currentKeyData.token || currentKeyData.token_id); - NotificationManager.success("Key deleted successfully"); - if (onDelete) { - onDelete(); - } - onClose(); - } catch (error) { - console.error("Error deleting the key:", error); - NotificationManager.fromBackend(error); - } - // Reset the confirmation input - setDeleteConfirmInput(""); - }; - - const copyToClipboard = async (text: string, key: string) => { - const success = await utilCopyToClipboard(text); - if (success) { - setCopiedStates((prev) => ({ ...prev, [key]: true })); - setTimeout(() => { - setCopiedStates((prev) => ({ ...prev, [key]: false })); - }, 2000); - } - }; - - const handleRegenerateKeyUpdate = (updatedKeyData: Partial) => { - // Update local state immediately with ALL the new data - setCurrentKeyData((prevData) => { - if (!prevData) return undefined; - const newData = { - ...prevData, - ...updatedKeyData, // This should include the new token (key-id) - // Update the created_at to show when it was regenerated - created_at: new Date().toLocaleString(), - }; - return newData; - }); - - // Track regeneration timestamp - setLastRegeneratedAt(new Date()); - setIsRecentlyRegenerated(true); - - if (onKeyDataUpdate) { - onKeyDataUpdate({ - ...updatedKeyData, - created_at: new Date().toLocaleString(), - }); - } - }; - - // Update the formatTimestamp function to use the desired date format - const formatTimestamp = (timestamp: string | Date) => { - const date = new Date(timestamp); - const dateStr = date.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - const timeStr = date.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - }); - return `${dateStr} at ${timeStr}`; - }; - - return ( -
-
-
- - {currentKeyData.key_alias || "API Key"} - -
-
- Key ID - {currentKeyData.token_id || currentKeyData.token} -
- : } - onClick={() => copyToClipboard(currentKeyData.token_id || currentKeyData.token, "key-id")} - className={`ml-2 transition-all duration-200${ - copiedStates["key-id"] - ? "text-green-600 bg-green-50 border-green-200" - : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" - }`} - /> -
- - {/* Add timestamp and regeneration indicator */} -
- - {currentKeyData.updated_at && currentKeyData.updated_at !== currentKeyData.created_at - ? `Updated: ${formatTimestamp(currentKeyData.updated_at)}` - : `Created: ${formatTimestamp(currentKeyData.created_at)}`} - - - {isRecentlyRegenerated && ( - - Recently Regenerated - - )} - - {lastRegeneratedAt && ( - - Regenerated - - )} -
-
- {userRole && rolesWithWriteAccess.includes(userRole) && ( -
- - - - - - -
- )} -
- - {/* Add RegenerateKeyModal */} - setIsRegenerateModalOpen(false)} - accessToken={accessToken} - premiumUser={premiumUser} - setAccessToken={setAccessToken} - onKeyUpdate={handleRegenerateKeyUpdate} - /> - - {/* Delete Confirmation Modal */} - {isDeleteModalOpen && - (() => { - const keyName = currentKeyData?.key_alias || currentKeyData?.token_id || "API Key"; - const isValid = deleteConfirmInput === keyName; - return ( -
-
-
-
-

Delete Key

- -
-
-
-
- - - -
-
-

- Warning: You are about to delete this API key. -

-

- This action is irreversible and will immediately revoke access for any applications using this - key. -

-
-
-

Are you sure you want to delete this API key?

-
- - setDeleteConfirmInput(e.target.value)} - placeholder="Enter key name exactly" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base" - autoFocus - /> -
-
-
-
- - -
-
-
- ); - })()} - - - - Overview - Settings - - - - {/* Overview Panel */} - - - - Spend -
- ${formatNumberWithCommas(currentKeyData.spend, 4)} - - of{" "} - {currentKeyData.max_budget !== null - ? `$${formatNumberWithCommas(currentKeyData.max_budget)}` - : "Unlimited"} - -
-
- - - Rate Limits -
- TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"} - RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"} -
-
- - - Models -
- {currentKeyData.models && currentKeyData.models.length > 0 ? ( - currentKeyData.models.map((model, index) => ( - - {model} - - )) - ) : ( - No models specified - )} -
-
- - - - - - - - -
-
- - {/* Settings Panel */} - - -
- Key Settings - {!isEditing && userRole && rolesWithWriteAccess.includes(userRole) && ( - - )} -
- - {isEditing ? ( - setIsEditing(false)} - onSubmit={handleKeyUpdate} - teams={teams} - accessToken={accessToken} - userID={userID} - userRole={userRole} - premiumUser={premiumUser} - /> - ) : ( -
-
- Key ID - {currentKeyData.token_id || currentKeyData.token} -
- -
- Key Alias - {currentKeyData.key_alias || "Not Set"} -
- -
- Secret Key - {currentKeyData.key_name} -
- -
- Team ID - {currentKeyData.team_id || "Not Set"} -
- -
- Organization - {currentKeyData.organization_id || "Not Set"} -
- -
- Created - {formatTimestamp(currentKeyData.created_at)} -
- - {lastRegeneratedAt && ( -
- Last Regenerated -
- {formatTimestamp(lastRegeneratedAt)} - - Recent - -
-
- )} - -
- Expires - {currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never"} -
- - - -
- Spend - ${formatNumberWithCommas(currentKeyData.spend, 4)} USD -
- -
- Budget - - {currentKeyData.max_budget !== null - ? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}` - : "Unlimited"} - -
- -
- Prompts - - {Array.isArray(currentKeyData.metadata?.prompts) && currentKeyData.metadata.prompts.length > 0 - ? currentKeyData.metadata.prompts.map((prompt, index) => ( - - {prompt} - - )) - : "No prompts specified"} - -
- -
- Models -
- {currentKeyData.models && currentKeyData.models.length > 0 ? ( - currentKeyData.models.map((model, index) => ( - - {model} - - )) - ) : ( - No models specified - )} -
-
- -
- Rate Limits - TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"} - RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"} - - Max Parallel Requests:{" "} - {currentKeyData.max_parallel_requests !== null - ? currentKeyData.max_parallel_requests - : "Unlimited"} - - - Model TPM Limits:{" "} - {currentKeyData.metadata?.model_tpm_limit - ? JSON.stringify(currentKeyData.metadata.model_tpm_limit) - : "Unlimited"} - - - Model RPM Limits:{" "} - {currentKeyData.metadata?.model_rpm_limit - ? JSON.stringify(currentKeyData.metadata.model_rpm_limit) - : "Unlimited"} - -
- -
- Metadata -
-                      {formatMetadataForDisplay(currentKeyData.metadata)}
-                    
-
- - - - -
- )} -
-
-
-
-
- ); -} - -export default KeyInfoView; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/SaveKeyModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/SaveKeyModal.tsx deleted file mode 100644 index fed678aadc..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/SaveKeyModal.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { Button, Col, Grid, Text, Title } from "@tremor/react"; -import { CopyToClipboard } from "react-copy-to-clipboard"; -import { Modal } from "antd"; -import React from "react"; -import NotificationsManager from "@/components/molecules/notifications_manager"; - -export interface SaveKeyModalProps { - apiKey: string; - isModalVisible: boolean; - handleOk: () => void; - handleCancel: () => void; -} - -const SaveKeyModal = ({ apiKey, isModalVisible, handleOk, handleCancel }: SaveKeyModalProps) => { - const handleCopy = () => { - NotificationsManager.success("API Key copied to clipboard"); - }; - - return ( - - - Save your Key - -

- Please save this secret key somewhere safe and accessible. For security reasons,{" "} - you will not be able to view it again through your LiteLLM account. If you lose this secret key, you - will need to generate a new one. -

- - - {apiKey != null ? ( -
- API Key: -
-
{apiKey}
-
- - - - -
- ) : ( - Key being created, this might take 30s - )} - -
-
- ); -}; - -export default SaveKeyModal; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx deleted file mode 100644 index 180d3336f1..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx +++ /dev/null @@ -1,667 +0,0 @@ -"use client"; -// TODO: refactor - -import React, { useEffect, useState } from "react"; -import { ColumnDef } from "@tanstack/react-table"; -import { Button } from "@tremor/react"; -import { Tooltip } from "antd"; -import { updateExistingKeys } from "@/utils/dataUtils"; -import { flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table"; -import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Icon } from "@tremor/react"; -import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"; -import { Badge, Text } from "@tremor/react"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; -import { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; -import { Organization, userListCall } from "@/components/networking"; -import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; -import FilterComponent, { FilterOption } from "@/components/molecules/filter"; -import { useFilterLogic } from "@/components/key_team_helpers/filter_logic"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import KeyInfoView from "@/app/(dashboard)/virtual-keys/components/KeyInfoView"; - -interface AllKeysTableProps { - keys: KeyResponse[]; - setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void; - isLoading?: boolean; - pagination: { - currentPage: number; - totalPages: number; - totalCount: number; - }; - onPageChange: (page: number) => void; - pageSize?: number; - organizations: Organization[] | null; - refresh?: () => void; - onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void; - currentSort?: { - sortBy: string; - sortOrder: "asc" | "desc"; - }; - setAccessToken?: (token: string) => void; -} - -interface UserResponse { - user_id: string; - user_email: string; - user_role: string; -} - -const VirtualKeysTable = ({ - keys, - setKeys, - isLoading = false, - pagination, - onPageChange, - pageSize = 50, - organizations, - refresh, - onSortChange, - currentSort, - setAccessToken, -}: AllKeysTableProps) => { - const { userId: userID, userRole, accessToken, premiumUser } = useAuthorized(); - const { teams } = useTeams(); - const [selectedKeyId, setSelectedKeyId] = useState(null); - const [userList, setUserList] = useState([]); - const [sorting, setSorting] = React.useState(() => { - if (currentSort) { - return [ - { - id: currentSort.sortBy, - desc: currentSort.sortOrder === "desc", - }, - ]; - } - return [ - { - id: "created_at", - desc: true, - }, - ]; - }); - const [expandedAccordions, setExpandedAccordions] = useState>({}); - - // Use the filter logic hook - - const { filters, filteredKeys, allKeyAliases, allTeams, allOrganizations, handleFilterChange, handleFilterReset } = - useFilterLogic({ - keys, - teams, - organizations, - accessToken, - }); - - useEffect(() => { - if (accessToken) { - const user_IDs = keys.map((key) => key.user_id).filter((id) => id !== null); - const fetchUserList = async () => { - const userListData = await userListCall(accessToken, user_IDs, 1, 100); - setUserList(userListData.users); - }; - fetchUserList(); - } - }, [accessToken, keys]); - - // Add a useEffect to call refresh when a key is created - useEffect(() => { - if (!refresh || typeof window === "undefined") { - return; - } - const handleStorageChange = () => { - refresh(); - }; - - // Listen for storage events that might indicate a key was created - window.addEventListener("storage", handleStorageChange); - - return () => { - window.removeEventListener("storage", handleStorageChange); - }; - }, [refresh]); - - const columns: ColumnDef[] = [ - { - id: "expander", - header: () => null, - cell: ({ row }) => - row.getCanExpand() ? ( - - ) : null, - }, - { - id: "token", - accessorKey: "token", - header: "Key ID", - cell: (info) => ( -
- - - -
- ), - }, - { - id: "key_alias", - accessorKey: "key_alias", - header: "Key Alias", - cell: (info) => { - const value = info.getValue() as string; - return ( - {value ? (value.length > 20 ? `${value.slice(0, 20)}...` : value) : "-"} - ); - }, - }, - { - id: "key_name", - accessorKey: "key_name", - header: "Secret Key", - cell: (info) => {info.getValue() as string}, - }, - { - id: "team_alias", - accessorKey: "team_id", - header: "Team Alias", - cell: ({ row, getValue }) => { - const teamId = getValue() as string; - const team = teams?.find((t) => t.team_id === teamId); - return team?.team_alias || "Unknown"; - }, - }, - { - id: "team_id", - accessorKey: "team_id", - header: "Team ID", - cell: (info) => ( - - {info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"} - - ), - }, - { - id: "organization_id", - accessorKey: "organization_id", - header: "Organization ID", - cell: (info) => (info.getValue() ? info.renderValue() : "-"), - }, - { - id: "user_email", - accessorKey: "user_id", - header: "User Email", - cell: (info) => { - const userId = info.getValue() as string; - const user = userList.find((u) => u.user_id === userId); - return user?.user_email ? ( - - {user?.user_email.slice(0, 20)}... - - ) : ( - "-" - ); - }, - }, - { - id: "user_id", - accessorKey: "user_id", - header: "User ID", - cell: (info) => { - const userId = info.getValue() as string | null; - if (userId && userId.length > 15) { - return ( - - {userId.slice(0, 7)}... - - ); - } - return userId ? userId : "-"; - }, - }, - { - id: "created_at", - accessorKey: "created_at", - header: "Created At", - cell: (info) => { - const value = info.getValue(); - return value ? new Date(value as string).toLocaleDateString() : "-"; - }, - }, - { - id: "created_by", - accessorKey: "created_by", - header: "Created By", - cell: (info) => { - const value = info.getValue() as string | null; - if (value && value.length > 15) { - return ( - - {value.slice(0, 7)}... - - ); - } - return value; - }, - }, - { - id: "updated_at", - accessorKey: "updated_at", - header: "Updated At", - cell: (info) => { - const value = info.getValue(); - return value ? new Date(value as string).toLocaleDateString() : "Never"; - }, - }, - { - id: "expires", - accessorKey: "expires", - header: "Expires", - cell: (info) => { - const value = info.getValue(); - return value ? new Date(value as string).toLocaleDateString() : "Never"; - }, - }, - { - id: "spend", - accessorKey: "spend", - header: "Spend (USD)", - cell: (info) => formatNumberWithCommas(info.getValue() as number, 4), - }, - { - id: "max_budget", - accessorKey: "max_budget", - header: "Budget (USD)", - cell: (info) => { - const maxBudget = info.getValue() as number | null; - if (maxBudget === null) { - return "Unlimited"; - } - return `$${formatNumberWithCommas(maxBudget)}`; - }, - }, - { - id: "budget_reset_at", - accessorKey: "budget_reset_at", - header: "Budget Reset", - cell: (info) => { - const value = info.getValue(); - return value ? new Date(value as string).toLocaleString() : "Never"; - }, - }, - { - id: "models", - accessorKey: "models", - header: "Models", - cell: (info) => { - const models = info.getValue() as string[]; - return ( -
- {Array.isArray(models) ? ( -
- {models.length === 0 ? ( - - All Proxy Models - - ) : ( - <> -
- {models.length > 3 && ( -
- { - setExpandedAccordions((prev) => ({ - ...prev, - [info.row.id]: !prev[info.row.id], - })); - }} - /> -
- )} -
- {models.slice(0, 3).map((model, index) => - model === "all-proxy-models" ? ( - - All Proxy Models - - ) : ( - - - {model.length > 30 - ? `${getModelDisplayName(model).slice(0, 30)}...` - : getModelDisplayName(model)} - - - ), - )} - {models.length > 3 && !expandedAccordions[info.row.id] && ( - - - +{models.length - 3} {models.length - 3 === 1 ? "more model" : "more models"} - - - )} - {expandedAccordions[info.row.id] && ( -
- {models.slice(3).map((model, index) => - model === "all-proxy-models" ? ( - - All Proxy Models - - ) : ( - - - {model.length > 30 - ? `${getModelDisplayName(model).slice(0, 30)}...` - : getModelDisplayName(model)} - - - ), - )} -
- )} -
-
- - )} -
- ) : null} -
- ); - }, - }, - { - id: "rate_limits", - header: "Rate Limits", - cell: ({ row }) => { - const key = row.original; - return ( -
-
TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}
-
RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}
-
- ); - }, - }, - ]; - - const filterOptions: FilterOption[] = [ - { - name: "Team ID", - label: "Team ID", - isSearchable: true, - searchFn: async (searchText: string) => { - if (!allTeams || allTeams.length === 0) return []; - - const filteredTeams = allTeams.filter( - (team) => - team.team_id.toLowerCase().includes(searchText.toLowerCase()) || - (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())), - ); - - return filteredTeams.map((team) => ({ - label: `${team.team_alias || team.team_id} (${team.team_id})`, - value: team.team_id, - })); - }, - }, - { - name: "Organization ID", - label: "Organization ID", - isSearchable: true, - searchFn: async (searchText: string) => { - if (!allOrganizations || allOrganizations.length === 0) return []; - - const filteredOrgs = allOrganizations.filter( - (org) => org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false, - ); - - return filteredOrgs - .filter((org) => org.organization_id !== null && org.organization_id !== undefined) - .map((org) => ({ - label: `${org.organization_id || "Unknown"} (${org.organization_id})`, - value: org.organization_id as string, - })); - }, - }, - { - name: "Key Alias", - label: "Key Alias", - isSearchable: true, - searchFn: async (searchText) => { - const filteredKeyAliases = allKeyAliases.filter((key) => { - return key.toLowerCase().includes(searchText.toLowerCase()); - }); - - return filteredKeyAliases.map((key) => { - return { - label: key, - value: key, - }; - }); - }, - }, - { - name: "User ID", - label: "User ID", - isSearchable: false, - }, - { - name: "Key Hash", - label: "Key Hash", - isSearchable: false, - }, - ]; - - console.log(`keys: ${JSON.stringify(keys)}`); - - const table = useReactTable({ - data: filteredKeys, - columns: columns.filter((col) => col.id !== "expander"), - state: { - sorting, - }, - onSortingChange: (updaterOrValue) => { - const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue; - console.log(`newSorting: ${JSON.stringify(newSorting)}`); - setSorting(newSorting); - if (newSorting && newSorting.length > 0) { - const sortState = newSorting[0]; - const sortBy = sortState.id; - const sortOrder = sortState.desc ? "desc" : "asc"; - console.log(`sortBy: ${sortBy}, sortOrder: ${sortOrder}`); - handleFilterChange({ - ...filters, - "Sort By": sortBy, - "Sort Order": sortOrder, - }); - onSortChange?.(sortBy, sortOrder); - } - }, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - enableSorting: true, - manualSorting: false, - }); - - // Update local sorting state when currentSort prop changes - React.useEffect(() => { - if (currentSort) { - setSorting([ - { - id: currentSort.sortBy, - desc: currentSort.sortOrder === "desc", - }, - ]); - } - }, [currentSort]); - - return ( -
- {selectedKeyId ? ( - setSelectedKeyId(null)} - keyData={filteredKeys.find((k) => k.token === selectedKeyId)} - onKeyDataUpdate={(updatedKeyData) => { - setKeys((keys) => - keys.map((key) => { - if (key.token === updatedKeyData.token) { - return updateExistingKeys(key, updatedKeyData); - } - return key; - }), - ); - if (refresh) refresh(); // Minimal fix: refresh the full key list after an update - }} - onDelete={() => { - setKeys((keys) => keys.filter((key) => key.token !== selectedKeyId)); - if (refresh) refresh(); // Minimal fix: refresh the full key list after a delete - }} - accessToken={accessToken} - userID={userID} - userRole={userRole} - teams={allTeams} - premiumUser={premiumUser} - setAccessToken={setAccessToken} - /> - ) : ( -
-
- -
- -
- - Showing{" "} - {isLoading - ? "..." - : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`}{" "} - of {isLoading ? "..." : pagination.totalCount} results - - -
- - Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages} - - - - - -
-
-
-
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - -
-
- {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} -
- {header.id !== "actions" && ( -
- {header.column.getIsSorted() ? ( - { - asc: , - desc: , - }[header.column.getIsSorted() as string] - ) : ( - - )} -
- )} -
-
- ))} -
- ))} -
- - {isLoading ? ( - - -
-

🚅 Loading keys...

-
-
-
- ) : filteredKeys.length > 0 ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - 3 ? "px-0" : ""}`} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - -
-

No keys found

-
-
-
- )} -
-
-
-
-
-
- )} -
- ); -}; - -export default VirtualKeysTable; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts deleted file mode 100644 index ac174a554e..0000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { useCallback, useEffect, useState, useRef } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { debounce } from "lodash"; -import { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; -import { keyListCall, Organization } from "@/components/networking"; -import { defaultPageSize } from "@/components/constants"; -import { fetchAllKeyAliases, fetchAllOrganizations, fetchAllTeams } from "@/components/key_team_helpers/filter_helpers"; - -export interface FilterState { - "Team ID": string; - "Organization ID": string; - "Key Alias": string; - [key: string]: string; - "User ID": string; - "Sort By": string; - "Sort Order": string; -} - -export function useFilterLogic({ - keys, - teams, - organizations, - accessToken, -}: { - keys: KeyResponse[]; - teams: Team[] | null; - organizations: Organization[] | null; - accessToken: string | null; -}) { - const defaultFilters: FilterState = { - "Team ID": "", - "Organization ID": "", - "Key Alias": "", - "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", - }; - const [filters, setFilters] = useState(defaultFilters); - const [allTeams, setAllTeams] = useState(teams || []); - const [allOrganizations, setAllOrganizations] = useState(organizations || []); - const [filteredKeys, setFilteredKeys] = useState(keys); - const lastSearchTimestamp = useRef(0); - const debouncedSearch = useCallback( - debounce(async (filters: FilterState) => { - if (!accessToken) { - return; - } - - const currentTimestamp = Date.now(); - lastSearchTimestamp.current = currentTimestamp; - - try { - // Make the API call using userListCall with all filter parameters - const data = await keyListCall( - accessToken, - filters["Organization ID"] || null, - filters["Team ID"] || null, - filters["Key Alias"] || null, - filters["User ID"] || null, - filters["Key Hash"] || null, - 1, // Reset to first page when searching - defaultPageSize, - filters["Sort By"] || null, - filters["Sort Order"] || null, - ); - - // Only update state if this is the most recent search - if (currentTimestamp === lastSearchTimestamp.current) { - if (data) { - setFilteredKeys(data.keys); - console.log("called from debouncedSearch filters:", JSON.stringify(filters)); - console.log("called from debouncedSearch data:", JSON.stringify(data)); - } - } - } catch (error) { - console.error("Error searching users:", error); - } - }, 300), - [accessToken], - ); - // Apply filters to keys whenever keys or filters change - useEffect(() => { - if (!keys) { - setFilteredKeys([]); - return; - } - - let result = [...keys]; - - // Apply Team ID filter - if (filters["Team ID"]) { - result = result.filter((key) => key.team_id === filters["Team ID"]); - } - - // Apply Organization ID filter - if (filters["Organization ID"]) { - result = result.filter((key) => key.organization_id === filters["Organization ID"]); - } - - setFilteredKeys(result); - }, [keys, filters]); - - // Fetch all data for filters when component mounts - useEffect(() => { - const loadAllFilterData = async () => { - // Load all teams - no organization filter needed here - const teamsData = await fetchAllTeams(accessToken); - if (teamsData.length > 0) { - setAllTeams(teamsData); - } - - // Load all organizations - const orgsData = await fetchAllOrganizations(accessToken); - if (orgsData.length > 0) { - setAllOrganizations(orgsData); - } - }; - - if (accessToken) { - loadAllFilterData(); - } - }, [accessToken]); - - const queryAllKeysQuery = useQuery({ - queryKey: ["allKeys"], - queryFn: async () => { - if (!accessToken) throw new Error("Access token required"); - return await fetchAllKeyAliases(accessToken); - }, - enabled: !!accessToken, - }); - const allKeyAliases = queryAllKeysQuery.data || []; - - // Update teams and organizations when props change - useEffect(() => { - if (teams && teams.length > 0) { - setAllTeams((prevTeams) => { - // Only update if we don't already have a larger set of teams - return prevTeams.length < teams.length ? teams : prevTeams; - }); - } - }, [teams]); - - useEffect(() => { - if (organizations && organizations.length > 0) { - setAllOrganizations((prevOrgs) => { - // Only update if we don't already have a larger set of organizations - return prevOrgs.length < organizations.length ? organizations : prevOrgs; - }); - } - }, [organizations]); - - const handleFilterChange = (newFilters: Record) => { - // Update filters state - setFilters({ - "Team ID": newFilters["Team ID"] || "", - "Organization ID": newFilters["Organization ID"] || "", - "Key Alias": newFilters["Key Alias"] || "", - "User ID": newFilters["User ID"] || "", - "Sort By": newFilters["Sort By"] || "created_at", - "Sort Order": newFilters["Sort Order"] || "desc", - }); - - // Fetch keys based on new filters - const updatedFilters = { - ...filters, - ...newFilters, - }; - debouncedSearch(updatedFilters); - }; - - const handleFilterReset = () => { - // Reset filters state - setFilters(defaultFilters); - - // Reset selections - debouncedSearch(defaultFilters); - }; - - return { - filters, - filteredKeys, - allKeyAliases, - allTeams, - allOrganizations, - handleFilterChange, - handleFilterReset, - }; -} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx index 611d92f1a3..56f780b296 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx @@ -1,16 +1,18 @@ "use client"; -import VirtualKeysTable from "@/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable"; import { useState } from "react"; -import useKeyList from "@/components/key_team_helpers/key_list"; +import useKeyList, { KeyResponse } from "@/components/key_team_helpers/key_list"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Col, Grid } from "@tremor/react"; -import CreateKey from "@/app/(dashboard)/virtual-keys/components/CreateKey"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import UserDashboard from "@/components/user_dashboard"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; +import { Organization } from "@/components/networking"; const VirtualKeysPage = () => { - const { accessToken, userRole } = useAuthorized(); + const { accessToken, userRole, userId, premiumUser, userEmail } = useAuthorized(); + const { teams, setTeams } = useTeams(); const [createClicked, setCreateClicked] = useState(false); + const [organizations, setOrganizations] = useState([]); const queryClient = new QueryClient(); @@ -28,20 +30,21 @@ const VirtualKeysPage = () => { return ( -
- - - - {}} - organizations={null} - /> - - -
+ {}} + setUserEmail={() => {}} + setTeams={setTeams} + setKeys={setKeys} + premiumUser={premiumUser} + organizations={organizations} + addKey={addKey} + createClicked={createClicked} + />
); }; diff --git a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx index d146d5ba79..86903d5553 100644 --- a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx @@ -90,7 +90,7 @@ interface ViewKeyTableProps { selectedTeam: Team | null; setSelectedTeam: React.Dispatch>; data: KeyResponse[] | null; - setData: React.Dispatch>; + setData: (keys: KeyResponse[]) => void; teams: Team[] | null; premiumUser: boolean; currentOrg: Organization | null; @@ -135,13 +135,6 @@ interface CombinedLimits { [key: string]: CombinedLimit; // Index signature allowing string keys } -/** - * ───────────────────────────────────────────────────────────────────────── - * @deprecated - * This component is being DEPRECATED in favor of src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/ - * Please contribute to the new refactor. - * ───────────────────────────────────────────────────────────────────────── - */ const ViewKeyTable: React.FC = ({ userID, userRole, diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index b6470e4952..4d98b37a8f 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -20,11 +20,12 @@ import ViewUserTeam from "./view_user_team"; import DashboardTeam from "./dashboard_default_team"; import Onboarding from "../app/onboarding/page"; import { useSearchParams, useRouter } from "next/navigation"; -import { Team } from "./key_team_helpers/key_list"; +import { KeyResponse, Team } from "./key_team_helpers/key_list"; import { jwtDecode } from "jwt-decode"; import { Typography } from "antd"; import { clearTokenCookies } from "@/utils/cookieUtils"; import { clearMCPAuthTokens } from "./mcp_tools/mcp_auth_storage"; +import { Setter } from "@/types"; export interface ProxySettings { PROXY_BASE_URL: string | null; @@ -56,7 +57,7 @@ interface UserDashboardProps { setUserRole: React.Dispatch>; setUserEmail: React.Dispatch>; setTeams: React.Dispatch>; - setKeys: React.Dispatch>; + setKeys: (keys: KeyResponse[]) => void; premiumUser: boolean; organizations: Organization[] | null; addKey: (data: any) => void; diff --git a/ui/litellm-dashboard/src/components/view_users.tsx b/ui/litellm-dashboard/src/components/view_users.tsx index 19479ce21a..7fcb7f54b8 100644 --- a/ui/litellm-dashboard/src/components/view_users.tsx +++ b/ui/litellm-dashboard/src/components/view_users.tsx @@ -30,6 +30,8 @@ import { updateExistingKeys } from "@/utils/dataUtils"; import { useDebouncedState } from "@tanstack/react-pacer/debouncer"; import { isAdminRole } from "@/utils/roles"; import NotificationsManager from "./molecules/notifications_manager"; +import { Setter } from "@/types"; +import { KeyResponse } from "@/components/key_team_helpers/key_list"; interface ViewUserDashboardProps { accessToken: string | null; From 3beb4a0797baee8694804b02aaa171be312ba1de Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Wed, 8 Oct 2025 17:54:48 -0700 Subject: [PATCH 16/17] added reload to base URL when FF is turned off --- .../src/app/(dashboard)/layout.tsx | 2 +- ui/litellm-dashboard/src/app/page.tsx | 1 - .../src/hooks/useFeatureFlags.tsx | 25 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx index 3d38a7362b..68ab361356 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx @@ -63,7 +63,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { />
- +
{children}
diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index ceee52399e..cefa680fc8 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -23,7 +23,6 @@ import ModelHubTable from "@/components/model_hub_table"; import NewUsagePage from "@/components/new_usage"; import APIRef from "@/components/api_ref"; import ChatUI from "@/components/chat_ui/ChatUI"; -import Sidebar from "@/components/leftnav"; import Usage from "@/components/usage"; import CacheDashboard from "@/components/cache_dashboard"; import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking"; diff --git a/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx b/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx index f1914f463a..d3d676d2d2 100644 --- a/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx +++ b/ui/litellm-dashboard/src/hooks/useFeatureFlags.tsx @@ -1,5 +1,13 @@ "use client"; + +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 +}; + import React, { createContext, useContext, useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; // ⟵ add this type Flags = { refactoredUIFlag: boolean; @@ -48,6 +56,8 @@ function writeFlagSafely(v: boolean) { } export const FeatureFlagsProvider = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); // ⟵ add this + // Lazy init reads from localStorage only on the client const [refactoredUIFlag, setRefactoredUIFlagState] = useState(() => readFlagSafely()); @@ -73,6 +83,21 @@ export const FeatureFlagsProvider = ({ children }: { children: React.ReactNode } return () => window.removeEventListener("storage", onStorage); }, []); + // Redirect to base path the moment the flag is OFF. + useEffect(() => { + if (refactoredUIFlag) return; // only act when turned off + + const base = getBasePath(); + const normalize = (p: string) => (p.endsWith("/") ? p : p + "/"); + const current = normalize(window.location.pathname); + + // Avoid a redirect loop if we're already at the base path. + if (current !== base) { + // Replace so the "off" redirect doesn't pollute history. + router.replace(base); + } + }, [refactoredUIFlag, router]); + return ( {children} ); From ff4eda0ea67730da3363f2d151decb9ee3ce4e9c Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Wed, 8 Oct 2025 18:00:15 -0700 Subject: [PATCH 17/17] removed unscoped changes (will be added later) --- .../components/modals/CreateUserModal.tsx | 346 ------------------ .../src/components/leftnav.tsx | 41 +-- 2 files changed, 18 insertions(+), 369 deletions(-) delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx 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, }))} />