diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 2ddea2d1e3..b2e13bd522 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -21,7 +21,8 @@ import { ToolOutlined, UserOutlined, } from "@ant-design/icons"; -import { Badge, ConfigProvider, Layout, Menu } from "antd"; +import { ConfigProvider, Layout, Menu } from "antd"; +import type { MenuProps } from "antd"; import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "../utils/roles"; import UsageIndicator from "./usage_indicator"; const { Sider } = Layout; @@ -35,237 +36,336 @@ interface SidebarProps { collapsed?: boolean; } -// Create a more comprehensive menu item configuration +// Menu item configuration interface MenuItem { key: string; page: string; label: string | React.ReactNode; roles?: string[]; - children?: MenuItem[]; // Add children property for submenus + children?: MenuItem[]; icon?: React.ReactNode; } +// Group configuration +interface MenuGroup { + groupLabel: string; + items: MenuItem[]; + roles?: string[]; +} + 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[] = [ + // Navigate to page helper + const navigateToPage = (page: string) => { + const newSearchParams = new URLSearchParams(window.location.search); + newSearchParams.set("page", page); + window.history.pushState(null, "", `?${newSearchParams.toString()}`); + setPage(page); + }; + + // Menu groups organized by category + const menuGroups: MenuGroup[] = [ { - key: "api-keys", - page: "api-keys", - label: "Virtual Keys", - icon: , - }, - { - key: "llm-playground", - page: "llm-playground", - label: "Playground", - icon: , - roles: rolesWithWriteAccess, - }, - { - key: "models", - page: "models", - label: "Models + Endpoints", - icon: , - roles: rolesWithWriteAccess, - }, - { - key: "new_usage", - page: "new_usage", - label: ( - - Usage - - ), - icon: , - roles: [...all_admin_roles, ...internalUserRoles], - }, - { key: "teams", page: "teams", label: "Teams", icon: }, - { - key: "organizations", - page: "organizations", - label: "Organizations", - icon: , - roles: all_admin_roles, - }, - { - key: "users", - page: "users", - label: "Internal Users", - icon: , - roles: all_admin_roles, - }, - { - key: "budgets", - page: "budgets", - label: "Budgets", - icon: , - roles: all_admin_roles, - }, - { key: "api_ref", page: "api_ref", label: "API Reference", icon: }, - { - key: "model-hub-table", - page: "model-hub-table", - label: "AI Hub", - icon: , - }, - { key: "logs", page: "logs", label: "Logs", icon: }, - { - key: "guardrails", - page: "guardrails", - label: "Guardrails", - icon: , - roles: all_admin_roles, - }, - { - key: "mcp-servers", - page: "mcp-servers", - label: "MCP Servers", - icon: , - }, - { - key: "tools", - page: "tools", - label: "Tools", - icon: , - children: [ + groupLabel: "AI GATEWAY", + items: [ { - key: "search-tools", - page: "search-tools", - label: "Search Tools", - icon: , + key: "api-keys", + page: "api-keys", + label: "Virtual Keys", + icon: , }, { - key: "vector-stores", - page: "vector-stores", - label: "Vector Stores", - icon: , - roles: all_admin_roles, + key: "llm-playground", + page: "llm-playground", + label: "Playground", + icon: , + roles: rolesWithWriteAccess, }, - ], - }, - { - key: "experimental", - page: "experimental", - label: "Experimental", - icon: , - children: [ { - key: "caching", - page: "caching", - label: "Caching", - icon: , - roles: all_admin_roles, + key: "models", + page: "models", + label: "Models + Endpoints", + icon: , + roles: rolesWithWriteAccess, }, { key: "agents", page: "agents", label: "Agents", - icon: , + icon: , roles: rolesWithWriteAccess, }, { - key: "prompts", - page: "prompts", - label: "Prompts", - icon: , + key: "mcp-servers", + page: "mcp-servers", + label: "MCP Servers", + icon: , + }, + { + key: "guardrails", + page: "guardrails", + label: "Guardrails", + icon: , roles: all_admin_roles, }, { - key: "transform-request", - page: "transform-request", - label: "API Playground", - icon: , - roles: [...all_admin_roles, ...internalUserRoles], + key: "tools", + page: "tools", + label: "Tools", + icon: , + children: [ + { + key: "search-tools", + page: "search-tools", + label: "Search Tools", + icon: , + }, + { + key: "vector-stores", + page: "vector-stores", + label: "Vector Stores", + icon: , + roles: all_admin_roles, + }, + ], }, - { - key: "tag-management", - 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: , + groupLabel: "OBSERVABILITY", + items: [ + { + key: "new_usage", + page: "new_usage", + label: "Usage", + icon: , + roles: [...all_admin_roles, ...internalUserRoles], + }, + { + key: "logs", + page: "logs", + label: "Logs", + icon: , + }, + ], + }, + { + groupLabel: "ACCESS CONTROL", + items: [ + { + key: "users", + page: "users", + label: "Internal Users", + icon: , + roles: all_admin_roles, + }, + { + key: "teams", + page: "teams", + label: "Teams", + icon: , + }, + { + key: "organizations", + page: "organizations", + label: "Organizations", + icon: , + roles: all_admin_roles, + }, + { + key: "budgets", + page: "budgets", + label: "Budgets", + icon: , + roles: all_admin_roles, + }, + ], + }, + { + groupLabel: "DEVELOPER TOOLS", + items: [ + { + key: "api_ref", + page: "api_ref", + label: "API Reference", + icon: , + }, + { + key: "model-hub-table", + page: "model-hub-table", + label: "AI Hub", + icon: , + }, + { + key: "experimental", + page: "experimental", + label: "Experimental", + icon: , + children: [ + { + key: "caching", + page: "caching", + label: "Caching", + icon: , + roles: all_admin_roles, + }, + { + key: "prompts", + page: "prompts", + label: "Prompts", + icon: , + roles: all_admin_roles, + }, + { + key: "transform-request", + page: "transform-request", + label: "API Playground", + icon: , + roles: [...all_admin_roles, ...internalUserRoles], + }, + { + key: "tag-management", + page: "tag-management", + label: "Tag Management", + icon: , + roles: all_admin_roles, + }, + { + key: "4", + page: "usage", + label: "Old Usage", + icon: , + }, + ], + }, + ], + }, + { + groupLabel: "SETTINGS", roles: all_admin_roles, - children: [ + items: [ { - key: "router-settings", - page: "router-settings", - label: "Router Settings", - icon: , - roles: all_admin_roles, - }, - { - key: "logging-and-alerts", - page: "logging-and-alerts", - label: "Logging & Alerts", - icon: , - roles: all_admin_roles, - }, - { - key: "admin-panel", - page: "admin-panel", - label: "Admin Settings", - icon: , - roles: all_admin_roles, - }, - { - key: "cost-tracking", - page: "cost-tracking", - label: "Cost Tracking", - icon: , - roles: all_admin_roles, - }, - { - key: "ui-theme", - page: "ui-theme", - label: "UI Theme", - icon: , + key: "settings", + page: "settings", + label: "Settings", + icon: , roles: all_admin_roles, + children: [ + { + key: "router-settings", + page: "router-settings", + label: "Router Settings", + icon: , + roles: all_admin_roles, + }, + { + key: "logging-and-alerts", + page: "logging-and-alerts", + label: "Logging & Alerts", + icon: , + roles: all_admin_roles, + }, + { + key: "admin-panel", + page: "admin-panel", + label: "Admin Settings", + icon: , + roles: all_admin_roles, + }, + { + key: "cost-tracking", + page: "cost-tracking", + label: "Cost Tracking", + icon: , + roles: all_admin_roles, + }, + { + key: "ui-theme", + page: "ui-theme", + label: "UI Theme", + icon: , + roles: all_admin_roles, + }, + ], }, ], }, ]; - // Find the menu item that matches the default page, including in submenus - const findMenuItemKey = (page: string): string => { - // Check top-level items - const topLevelItem = menuItems.find((item) => item.page === page); - if (topLevelItem) return topLevelItem.key; - // Check submenu items - for (const item of menuItems) { - if (item.children) { - const childItem = item.children.find((child) => child.page === page); - if (childItem) return childItem.key; + // Filter items based on user role + const filterItemsByRole = (items: MenuItem[]): MenuItem[] => { + return items + .filter((item) => !item.roles || item.roles.includes(userRole)) + .map((item) => ({ + ...item, + children: item.children ? filterItemsByRole(item.children) : undefined, + })); + }; + + // Build menu items with groups + const buildMenuItems = (): MenuProps["items"] => { + const items: MenuProps["items"] = []; + + menuGroups.forEach((group) => { + // Check if group has role restriction + if (group.roles && !group.roles.includes(userRole)) { + return; + } + + const filteredItems = filterItemsByRole(group.items); + if (filteredItems.length === 0) return; + + // Add group with items + items.push({ + type: "group", + label: collapsed ? null : ( + + {group.groupLabel} + + ), + children: filteredItems.map((item) => ({ + 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, + })), + }); + }); + + return items; + }; + + // Find selected menu key + const findMenuItemKey = (page: string): string => { + for (const group of menuGroups) { + for (const item of group.items) { + if (item.page === page) return item.key; + if (item.children) { + const child = item.children.find((c) => c.page === page); + if (child) return child.key; + } } } - return "1"; // Default to first item if not found + return "api-keys"; }; const selectedMenuKey = findMenuItemKey(defaultSelectedKey); - const filteredMenuItems = menuItems.filter((item) => { - // Check if parent item has roles and user has access - const hasParentAccess = !item.roles || item.roles.includes(userRole); - - console.log(`Menu item ${item.label}: roles=${item.roles}, userRole=${userRole}, hasAccess=${hasParentAccess}`); - - if (!hasParentAccess) return false; - - // Filter children if they exist - if (item.children) { - item.children = item.children.filter((child) => !child.roles || child.roles.includes(userRole)); - } - - return true; - }); - return ( = ({ accessToken, setPage, userRole, defau collapsible trigger={null} style={{ - transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", // Material Design easing + transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", position: "relative", }} > @@ -284,8 +384,15 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau theme={{ components: { Menu: { - iconSize: 18, - fontSize: 14, + iconSize: 15, + fontSize: 13, + itemMarginInline: 4, + itemPaddingInline: 8, + itemHeight: 30, + itemBorderRadius: 6, + subMenuItemBorderRadius: 6, + groupTitleFontSize: 10, + groupTitleLineHeight: 1.5, }, }, }} @@ -293,38 +400,16 @@ const Sidebar: React.FC = ({ accessToken, setPage, userRole, defau ({ - key: item.key, - icon: item.icon, - label: item.label, - children: item.children?.map((child) => ({ - 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: !item.children - ? () => { - const newSearchParams = new URLSearchParams(window.location.search); - newSearchParams.set("page", item.page); - window.history.pushState(null, "", `?${newSearchParams.toString()}`); - setPage(item.page); - } - : undefined, - }))} + items={buildMenuItems()} /> {isAdminRole(userRole) && !collapsed && } diff --git a/ui/litellm-dashboard/src/components/navbar.tsx b/ui/litellm-dashboard/src/components/navbar.tsx index 9113a0f54c..2e0edcfcd8 100644 --- a/ui/litellm-dashboard/src/components/navbar.tsx +++ b/ui/litellm-dashboard/src/components/navbar.tsx @@ -43,11 +43,28 @@ const Navbar: React.FC = ({ }) => { const baseUrl = getProxyBaseUrl(); const [logoutUrl, setLogoutUrl] = useState(""); + const [version, setVersion] = useState(""); const { logoUrl } = useTheme(); // Simple logo URL: use custom logo if available, otherwise default const imageUrl = logoUrl || `${baseUrl}/get_image`; + useEffect(() => { + const fetchVersion = async () => { + try { + const response = await fetch(`${baseUrl}/health/readiness`); + const data = await response.json(); + if (data.litellm_version) { + setVersion(data.litellm_version); + } + } catch (error) { + console.error("Failed to fetch version:", error); + } + }; + + fetchVersion(); + }, [baseUrl]); + useEffect(() => { const initializeProxySettings = async () => { if (accessToken) { @@ -146,9 +163,21 @@ const Navbar: React.FC = ({ )} - - LiteLLM Brand - +
+ + LiteLLM Brand + + {version && ( + + v{version} + + )} +
{/* Right side nav items */}