diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 1c8f92b720f..59a747970f1 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1,9 +1,4 @@ { - "src/app/(dashboard)/api-reference/APIReferenceView.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/budgets/components/budget_modal.tsx": { "no-restricted-imports": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/APIReferenceView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/APIReferenceView.tsx index 5861cc87e5b..5784d529575 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/APIReferenceView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/APIReferenceView.tsx @@ -1,6 +1,7 @@ "use client"; + import React from "react"; -import { Text, Tab, TabGroup, TabList, TabPanel, TabPanels, Grid } from "@tremor/react"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import CodeBlock from "./components/CodeBlock"; import DocLink from "@/app/(dashboard)/api-reference/components/DocLink"; @@ -21,33 +22,28 @@ const APIReferenceView: React.FC = ({ proxySettings }) => { } return ( - <> - -
- {/* Header row with Docs link on the right */} -
-

- OpenAI Compatible Proxy: API Reference -

- -
+
+
+

OpenAI Compatible Proxy: API Reference

+ +
- - LiteLLM is OpenAI Compatible. This means your API Key works with the OpenAI SDK. Just replace the base_url - to point to your litellm proxy. Example Below{" "} - +

+ LiteLLM is OpenAI Compatible. This means your API Key works with the OpenAI SDK. Just replace the base_url to + point to your litellm proxy. Example Below +

- - - OpenAI Python SDK - LlamaIndex - Langchain Py - - - - + + OpenAI Python SDK + LlamaIndex + Langchain Py + + + + - + /> + - - + - + /> + - - + - - - -
- - + /> + + +
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/CodeBlock.tsx b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/CodeBlock.tsx index 8d2fa8acb5d..d040e210ba3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/CodeBlock.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-reference/components/CodeBlock.tsx @@ -1,7 +1,10 @@ +"use client"; + import { useState } from "react"; import { CheckIcon, ClipboardIcon } from "lucide-react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { Button } from "@/components/ui/button"; interface CodeBlockProps { code: string; @@ -17,14 +20,16 @@ const CodeBlock = ({ code, language }: CodeBlockProps) => { }; return ( -
- + {copied ? : } + { margin: 0, padding: "1.5rem", borderRadius: "0.5rem", - fontSize: "0.9rem", - backgroundColor: "#fafafa", + fontSize: "0.8rem", + backgroundColor: "transparent", }} showLineNumbers > diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx index 09951dc1923..4b9e1addce5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx @@ -123,9 +123,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
) : ( <> -
- -
+
{children}
)} diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx index dac4a35ebb6..da6ca8cc2c9 100644 --- a/ui/litellm-dashboard/src/components/leftnav.test.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx @@ -116,40 +116,8 @@ describe("Sidebar (leftnav)", () => { expect(screen.getByText("Search Tools")).toBeInTheDocument(); }); }); - it("has no duplicate keys among all menu items and their children", () => { - // Helper to recursively extract all keys from Ant Design Menu items - function getAllKeysFromMenu(wrapper: HTMLElement): string[] { - const allKeys: string[] = []; - // Ant Design renders key as data-menu-id or inside attributes, but for this case, we look for text as fallback. - // For a generic check, here we fetch ids from rendered list items, and also descend into submenus - const items = wrapper.querySelectorAll("[data-menu-id]"); - items.forEach((item) => { - const dataMenuId = item.getAttribute("data-menu-id"); - if (dataMenuId) { - allKeys.push(dataMenuId); - } - }); - return allKeys; - } - - const { container } = renderWithProviders(); - const allRenderedKeys = getAllKeysFromMenu(container); - - const keySet = new Set(); - const duplicates: string[] = []; - for (const key of allRenderedKeys) { - if (keySet.has(key)) { - duplicates.push(key); - } - keySet.add(key); - } - expect(duplicates).toHaveLength(0); - }); describe("Admin Viewer parity", () => { - // Admin Viewer follows a "read parity with Proxy Admin, no writes, no - // cost-incurring actions" rule. Playground stays hidden (incurs LLM - // cost); Models + Endpoints and Agents must be visible read-only. const adminViewerAuth = { userId: "admin-viewer-user-id", accessToken: "test-access-token", @@ -176,8 +144,6 @@ describe("Sidebar (leftnav)", () => { it("shows Agents (under Agentic) to Admin Viewer (read-only)", async () => { mockUseAuthorized.mockReturnValueOnce(adminViewerAuth); renderWithProviders(); - // Agents is now nested under the "Agentic" submenu — expand parent - // first to render the children, then assert Agents is visible. act(() => { fireEvent.click(screen.getByText("Agentic")); }); diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 74b45942006..c0c00266180 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -1,38 +1,39 @@ +"use client"; + import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { - ApiOutlined, - ApartmentOutlined, - AppstoreOutlined, - AuditOutlined, - BankOutlined, - BarChartOutlined, - BgColorsOutlined, - BlockOutlined, - BookOutlined, - CommentOutlined, - CreditCardOutlined, - DatabaseOutlined, - ExperimentOutlined, - ExportOutlined, - FileTextOutlined, - FolderOutlined, - KeyOutlined, - LineChartOutlined, - PlayCircleOutlined, - RobotOutlined, - SafetyOutlined, - SearchOutlined, - SettingOutlined, - TagsOutlined, - TeamOutlined, - ToolOutlined, - UserOutlined, -} from "@ant-design/icons"; -import type { MenuProps } from "antd"; -import { ConfigProvider, Layout, Menu } from "antd"; -import { useMemo } from "react"; + Key, + PlayCircle, + MessageSquare, + Blocks, + Bot, + GitBranch, + BookOpen, + Wrench, + Search, + Database, + Shield, + ClipboardCheck, + Code, + BarChart3, + LineChart, + Users, + Folder, + User, + Building2, + CreditCard, + LayoutGrid, + FlaskConical, + FileText, + Tags, + Settings, + Palette, + ExternalLink, + ChevronRight, +} from "lucide-react"; +import { useMemo, useState } from "react"; import { all_admin_roles, internalUserRoles, @@ -45,9 +46,11 @@ import NewBadge from "./common_components/NewBadge"; import type { Organization } from "./networking"; import UsageIndicator from "./UsageIndicator"; import { MIGRATED_PAGES, migratedHref, legacyPageHref } from "@/utils/migratedPages"; -const { Sider } = Layout; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { cn } from "@/lib/cva.config"; -// Define the props type interface SidebarProps { setPage: (page: string) => void; defaultSelectedKey: string; @@ -61,7 +64,6 @@ interface SidebarProps { allowVectorStoresForTeamAdmins?: boolean; } -// Menu item configuration interface MenuItem { key: string; page: string; @@ -72,14 +74,12 @@ interface MenuItem { external_url?: string; } -// Group configuration interface MenuGroup { groupLabel: string; items: MenuItem[]; roles?: string[]; } -// Menu groups organized by category - defined outside component for export const menuGroups: MenuGroup[] = [ { groupLabel: "AI GATEWAY", @@ -88,13 +88,13 @@ const menuGroups: MenuGroup[] = [ key: "api-keys", page: "api-keys", label: "Virtual Keys", - icon: , + icon: , }, { key: "llm-playground", page: "llm-playground", label: "Playground", - icon: , + icon: , roles: rolesWithWriteAccess, }, { @@ -105,43 +105,39 @@ const menuGroups: MenuGroup[] = [ Chat ), - icon: , + icon: , }, { key: "models", page: "models", label: "Models + Endpoints", - icon: , - // Admin Viewer can view models read-only (write actions are - // hidden inside the page); Playground above stays write-only. + icon: , roles: rolesAllowedToViewWriteScopedPages, }, { key: "agentic", page: "agentic", label: "Agentic", - icon: , + icon: , children: [ { key: "agents", page: "agents", label: "Agents", - icon: , - // Admin Viewer can view agents read-only (write actions are - // hidden inside the page); Playground above stays write-only. + icon: , roles: rolesAllowedToViewWriteScopedPages, }, { key: "workflows", page: "workflows", label: "Workflow Runs", - icon: , + icon: , }, { key: "memory", page: "memory", label: "Memory", - icon: , + icon: , }, ], }, @@ -149,51 +145,51 @@ const menuGroups: MenuGroup[] = [ key: "mcp-servers", page: "mcp-servers", label: "MCP Servers", - icon: , + icon: , }, { key: "skills", page: "skills", label: "Skills", - icon: , + icon: , roles: all_admin_roles, }, { key: "guardrails", page: "guardrails", label: "Guardrails", - icon: , + icon: , }, { key: "policies", page: "policies", - label: Policies, - icon: , + label: "Policies", + icon: , roles: all_admin_roles, }, { key: "tools", page: "tools", label: "Tools", - icon: , + icon: , children: [ { key: "search-tools", page: "search-tools", label: "Search Tools", - icon: , + icon: , }, { key: "vector-stores", page: "vector-stores", label: "Vector Stores", - icon: , + icon: , }, { key: "tool-policies", page: "tool-policies", label: "Tool Policies", - icon: , + icon: , }, ], }, @@ -205,7 +201,7 @@ const menuGroups: MenuGroup[] = [ { key: "new_usage", page: "new_usage", - icon: , + icon: , roles: [...all_admin_roles, ...internalUserRoles], label: "Usage", }, @@ -213,13 +209,13 @@ const menuGroups: MenuGroup[] = [ key: "logs", page: "logs", label: "Logs", - icon: , + icon: , }, { key: "guardrails-monitor", page: "guardrails-monitor", label: "Guardrails Monitor", - icon: , + icon: , roles: [...all_admin_roles, ...internalUserRoles], }, ], @@ -231,7 +227,7 @@ const menuGroups: MenuGroup[] = [ key: "teams", page: "teams", label: "Teams", - icon: , + icon: , }, { key: "projects", @@ -241,35 +237,35 @@ const menuGroups: MenuGroup[] = [ Projects ), - icon: , + icon: , roles: all_admin_roles, }, { key: "users", page: "users", label: "Internal Users", - icon: , + icon: , roles: all_admin_roles, }, { key: "organizations", page: "organizations", label: "Organizations", - icon: , + icon: , roles: all_admin_roles, }, { key: "access-groups", page: "access-groups", label: "Access Groups", - icon: , + icon: , roles: all_admin_roles, }, { key: "budgets", page: "budgets", label: "Budgets", - icon: , + icon: , roles: all_admin_roles, }, ], @@ -281,61 +277,60 @@ const menuGroups: MenuGroup[] = [ key: "api_ref", page: "api_ref", label: "API Reference", - icon: , + icon: , }, { key: "model-hub-table", page: "model-hub-table", label: "AI Hub", - icon: , + icon: , }, - { key: "learning-resources", page: "learning-resources", label: "Learning Resources", - icon: , + icon: , external_url: "https://models.litellm.ai/cookbook", }, { key: "experimental", page: "experimental", label: "Experimental", - icon: , + icon: , children: [ { key: "caching", page: "caching", label: "Caching", - icon: , + icon: , roles: all_admin_roles, }, { key: "prompts", page: "prompts", label: "Prompts", - icon: , + icon: , roles: all_admin_roles, }, { key: "transform-request", page: "transform-request", label: "API Playground", - icon: , + icon: , roles: [...all_admin_roles, ...internalUserRoles], }, { key: "tag-management", page: "tag-management", label: "Tag Management", - icon: , + icon: , roles: all_admin_roles, }, { key: "4", page: "usage", label: "Old Usage", - icon: , + icon: , }, ], }, @@ -353,21 +348,21 @@ const menuGroups: MenuGroup[] = [ Settings ), - icon: , + icon: , roles: all_admin_roles, children: [ { key: "router-settings", page: "router-settings", label: "Router Settings", - icon: , + icon: , roles: all_admin_roles, }, { key: "logging-and-alerts", page: "logging-and-alerts", label: "Logging & Alerts", - icon: , + icon: , roles: all_admin_roles, }, { @@ -381,21 +376,21 @@ const menuGroups: MenuGroup[] = [ ), - icon: , + icon: , roles: all_admin_roles, }, { key: "cost-tracking", page: "cost-tracking", label: "Cost Tracking", - icon: , + icon: , roles: all_admin_roles, }, { key: "ui-theme", page: "ui-theme", label: "UI Theme", - icon: , + icon: , roles: all_admin_roles, }, ], @@ -404,6 +399,132 @@ const menuGroups: MenuGroup[] = [ }, ]; +function NavItem({ + item, + isSelected, + collapsed, + onNavigate, +}: { + item: MenuItem; + isSelected: boolean; + collapsed: boolean; + onNavigate: (page: string) => void; +}) { + const migratedRoute = MIGRATED_PAGES[item.page]; + const href = item.external_url ?? (migratedRoute ? migratedHref(migratedRoute) : legacyPageHref(item.page)); + const isExternal = Boolean(item.external_url); + + const handleClick = (e: React.MouseEvent) => { + if (isExternal) { + return; + } + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + return; + } + e.preventDefault(); + onNavigate(item.page); + }; + + const content = ( + + {item.icon} + {!collapsed && ( + + {item.label} + {isExternal && } + + )} + + ); + + if (collapsed) { + const tooltipLabel = typeof item.label === "string" ? item.label : item.page; + return ( + + {content} + {tooltipLabel} + + ); + } + + return content; +} + +function CollapsibleNavItem({ + item, + selectedKey, + collapsed, + onNavigate, +}: { + item: MenuItem; + selectedKey: string; + collapsed: boolean; + onNavigate: (page: string) => void; +}) { + const hasSelectedChild = item.children?.some((child) => child.key === selectedKey) ?? false; + const [open, setOpen] = useState(hasSelectedChild); + + if (collapsed) { + const tooltipLabel = typeof item.label === "string" ? item.label : item.page; + return ( + + + + + {tooltipLabel} + + ); + } + + return ( + + + + + +
+ {item.children?.map((child) => ( + + ))} +
+
+
+ ); +} + const Sidebar: React.FC = ({ setPage, defaultSelectedKey, @@ -420,7 +541,6 @@ const Sidebar: React.FC = ({ const { data: organizations } = useOrganizations(); const { data: teams } = useTeams(); - // Check if user is an org_admin const isOrgAdmin = useMemo(() => { if (!userId || !organizations) return false; return organizations.some((org: Organization) => @@ -428,55 +548,38 @@ const Sidebar: React.FC = ({ ); }, [userId, organizations]); - // Check if user is a team admin for any team const isTeamAdmin = useMemo(() => isUserTeamAdminForAnyTeam(teams ?? null, userId ?? ""), [teams, userId]); - // The parent (legacy root page or dashboard layout) owns navigation for both - // migrated and legacy pages; the sidebar only reports the selected page. const navigateToPage = (page: string) => setPage(page); - // Wrap label in so every nav item supports right-click → "Open in new tab" - // and Ctrl/Cmd+click to open in a new tab, while preserving SPA navigation for normal clicks. - const renderNavLink = (label: React.ReactNode, page: string, externalUrl?: string): React.ReactNode => { - if (externalUrl) { - return ( - e.stopPropagation()} - style={{ color: "inherit", textDecoration: "none" }} - > - {label} - - ); - } - const migratedRoute = MIGRATED_PAGES[page]; - const href = migratedRoute ? migratedHref(migratedRoute) : legacyPageHref(page); - return ( - { - if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { - e.stopPropagation(); - return; - } - e.preventDefault(); - }} - style={{ color: "inherit", textDecoration: "none" }} - > - {label} - - ); + const isFeatureGated = (item: MenuItem): boolean => { + if (item.key === "projects" && !enableProjectsUI) return true; + if (item.key === "chat" && !enableChatUI) return true; + return false; + }; + + const isDisabledForNonAdmin = (item: MenuItem): boolean => { + if (item.key === "agents" && disableAgentsForInternalUsers && !(allowAgentsForTeamAdmins && isTeamAdmin)) + return true; + if ( + item.key === "vector-stores" && + disableVectorStoresForInternalUsers && + !(allowVectorStoresForTeamAdmins && isTeamAdmin) + ) + return true; + return false; + }; + + const isVisibleForInternalUser = (item: MenuItem, enabledPages: string[]): boolean => { + if (item.children && item.children.length > 0) { + return item.children.some((child) => enabledPages.includes(child.page)); + } + return enabledPages.includes(item.page); }; - // Filter items based on user role and enabled pages for internal users const filterItemsByRole = (items: MenuItem[]): MenuItem[] => { const isAdmin = isAdminRole(userRole); - - // Debug logging - if (enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) { - } + const hasPageRestrictions = !isAdmin && enabledPagesInternalUsers != null; return items .map((item) => ({ @@ -484,127 +587,21 @@ const Sidebar: React.FC = ({ children: item.children ? filterItemsByRole(item.children) : undefined, })) .filter((item) => { - // Special handling for organizations and users menu items - allow org_admins if (item.key === "organizations" || item.key === "users") { const hasRoleAccess = !item.roles || item.roles.includes(userRole) || isOrgAdmin; if (!hasRoleAccess) return false; - - // Check enabled pages for internal users (non-admins) - if (!isAdmin && enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) { - const isIncluded = enabledPagesInternalUsers.includes(item.page); - return isIncluded; - } + if (hasPageRestrictions) return enabledPagesInternalUsers!.includes(item.page); return true; } - // Hide Projects page if enableProjectsUI is not enabled - if (item.key === "projects" && !enableProjectsUI) return false; - - // Hide Chat page if enableChatUI is not enabled - if (item.key === "chat" && !enableChatUI) return false; - - // Hide agents and vector-stores pages for non-admin users when disabled, - // unless allow_*_for_team_admins is on and the user is a team admin. - if ( - !isAdmin && - item.key === "agents" && - disableAgentsForInternalUsers && - !(allowAgentsForTeamAdmins && isTeamAdmin) - ) - return false; - if ( - !isAdmin && - item.key === "vector-stores" && - disableVectorStoresForInternalUsers && - !(allowVectorStoresForTeamAdmins && isTeamAdmin) - ) - return false; - - // Existing role check + if (isFeatureGated(item)) return false; + if (!isAdmin && isDisabledForNonAdmin(item)) return false; if (item.roles && !item.roles.includes(userRole)) return false; - - // Check enabled pages for internal users (non-admins) - if (!isAdmin && enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) { - // If item has children, check if any children are visible - if (item.children && item.children.length > 0) { - const hasVisibleChildren = item.children.some((child) => enabledPagesInternalUsers.includes(child.page)); - if (hasVisibleChildren) { - return true; - } - } - - const isIncluded = enabledPagesInternalUsers.includes(item.page); - return isIncluded; - } - + if (hasPageRestrictions) return isVisibleForInternalUser(item, enabledPagesInternalUsers!); return true; }); }; - // 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: renderNavLink(item.label, item.page, item.external_url), - children: item.children?.map((child) => ({ - key: child.key, - icon: child.icon, - label: renderNavLink(child.label, child.page, child.external_url), - onClick: () => { - if (child.external_url) { - window.open(child.external_url, "_blank"); - } else { - navigateToPage(child.page); - } - }, - })), - onClick: !item.children - ? () => { - if (item.external_url) { - window.open(item.external_url, "_blank"); - } else { - 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) { @@ -621,58 +618,58 @@ const Sidebar: React.FC = ({ const selectedMenuKey = findMenuItemKey(defaultSelectedKey); return ( - - + + ); }; export default Sidebar; -// Also export menuGroups for advanced use cases export { menuGroups };