diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx index 8b934e10779..8ab54ca6bd7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx @@ -1,4 +1,9 @@ +"use client"; + import Sidebar from "@/components/leftnav"; +import { getUISettings } from "@/components/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useEffect, useState } from "react"; interface SidebarProviderProps { setPage: (page: string) => void; @@ -7,7 +12,34 @@ interface SidebarProviderProps { } const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: SidebarProviderProps) => { - return ; + const { accessToken } = useAuthorized(); + const [enabledPagesInternalUsers, setEnabledPagesInternalUsers] = useState(null); + + useEffect(() => { + const fetchUISettings = async () => { + if (!accessToken) return; + + try { + const settings = await getUISettings(accessToken); + if (settings?.settings?.enabled_ui_pages_internal_users !== undefined) { + setEnabledPagesInternalUsers(settings.settings.enabled_ui_pages_internal_users); + } + } catch (error) { + console.error("Failed to fetch UI settings:", error); + } + }; + + fetchUISettings(); + }, [accessToken]); + + return ( + + ); }; export default SidebarProvider; diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index f24f4d60219..442ec24a2a0 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -39,6 +39,7 @@ interface SidebarProps { setPage: (page: string) => void; defaultSelectedKey: string; collapsed?: boolean; + enabledPagesInternalUsers?: string[] | null; } // Menu item configuration @@ -59,28 +60,8 @@ interface MenuGroup { roles?: string[]; } -const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapsed = false }) => { - const { userId, accessToken, userRole } = useAuthorized(); - const { data: organizations } = useOrganizations(); - - // Check if user is an org_admin - const isOrgAdmin = useMemo(() => { - if (!userId || !organizations) return false; - return organizations.some((org: Organization) => - org.members?.some((member) => member.user_id === userId && member.user_role === "org_admin"), - ); - }, [userId, organizations]); - - // 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[] = [ +// Menu groups organized by category - defined outside component for export +const menuGroups: MenuGroup[] = [ { groupLabel: "AI GATEWAY", items: [ @@ -337,15 +318,53 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse }, ]; - // Filter items based on user role +const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapsed = false, enabledPagesInternalUsers }) => { + const { userId, accessToken, userRole } = useAuthorized(); + const { data: organizations } = useOrganizations(); + + // Check if user is an org_admin + const isOrgAdmin = useMemo(() => { + if (!userId || !organizations) return false; + return organizations.some((org: Organization) => + org.members?.some((member) => member.user_id === userId && member.user_role === "org_admin"), + ); + }, [userId, organizations]); + + // 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); + }; + + // Filter items based on user role and enabled pages for internal users const filterItemsByRole = (items: MenuItem[]): MenuItem[] => { + const isAdmin = isAdminRole(userRole); + return items .filter((item) => { // Special handling for organizations menu item - allow org_admins if (item.key === "organizations") { - return !item.roles || item.roles.includes(userRole) || isOrgAdmin; + 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) { + return enabledPagesInternalUsers.includes(item.page); + } + return true; } - return !item.roles || item.roles.includes(userRole); + + // Existing role check + if (item.roles && !item.roles.includes(userRole)) return false; + + // Check enabled pages for internal users (non-admins) + if (!isAdmin && enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) { + return enabledPagesInternalUsers.includes(item.page); + } + + return true; }) .map((item) => ({ ...item, @@ -485,3 +504,6 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse }; export default Sidebar; + +// Also export menuGroups for advanced use cases +export { menuGroups }; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index a1625b6ffbe..758358b8b0d 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -5330,6 +5330,66 @@ export const getProxyUISettings = async (accessToken: string) => { } }; +export const getUISettings = async (accessToken: string) => { + /** + * Get UI-specific configuration flags from the database + */ + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/get/ui_settings` : `/get/ui_settings`; + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + console.error("Failed to get UI settings:", errorMessage); + return null; + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to get UI settings:", error); + return null; + } +}; + +export const updateUISettings = async (accessToken: string, settings: any) => { + /** + * Update UI-specific configuration flags in the database + * Only proxy admins can update these settings + */ + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/update/ui_settings` : `/update/ui_settings`; + const response = await fetch(url, { + method: "PATCH", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(settings), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to update UI settings:", error); + throw error; + } +}; + export const getGuardrailsList = async (accessToken: string) => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/v2/guardrails/list` : `/v2/guardrails/list`; diff --git a/ui/litellm-dashboard/src/components/page_metadata.ts b/ui/litellm-dashboard/src/components/page_metadata.ts new file mode 100644 index 00000000000..2e67f5baa22 --- /dev/null +++ b/ui/litellm-dashboard/src/components/page_metadata.ts @@ -0,0 +1,41 @@ +/** + * Page metadata for UI Settings configuration + * This file contains descriptions and metadata for all navigation pages + */ + +// Page descriptions for UI Settings configuration +export const pageDescriptions: Record = { + "api-keys": "Manage virtual keys for API access and authentication", + playground: "Interactive playground for testing LLM requests", + "models-and-endpoints": "Configure and manage LLM models and endpoints", + agents: "Create and manage AI agents", + "mcp-servers": "Configure Model Context Protocol servers", + guardrails: "Set up content moderation and safety guardrails", + policies: "Define access control and usage policies", + "search-tools": "Configure RAG search and retrieval tools", + "vector-stores": "Manage vector databases for embeddings", + usage: "View usage analytics and metrics", + logs: "Access request and response logs", + "internal-users": "Manage internal user accounts and permissions", + teams: "Create and manage teams for access control", + organizations: "Manage organizations and their members", + budgets: "Set and monitor spending budgets", + "api-reference": "Browse API documentation and endpoints", + "ai-hub": "Explore available AI models and providers", + "learning-resources": "Access tutorials and documentation", + caching: "Configure response caching settings", + "transform-request": "Set up request transformation rules", + "pass-through-endpoints": "Configure pass-through API endpoints", + "cost-tracking": "Track and analyze API costs", + "ui-themes": "Customize dashboard appearance", + "tag-management": "Organize resources with tags", + prompts: "Manage and version prompt templates", + "claude-code-plugins": "Configure Claude Code plugins", +}; + +export interface PageMetadata { + page: string; + label: string; + group: string; + description: string; +} diff --git a/ui/litellm-dashboard/src/components/page_utils.ts b/ui/litellm-dashboard/src/components/page_utils.ts new file mode 100644 index 00000000000..ca15c6257b6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/page_utils.ts @@ -0,0 +1,45 @@ +/** + * Utility functions for working with navigation pages + */ + +import { menuGroups } from "./leftnav"; +import { pageDescriptions, PageMetadata } from "./page_metadata"; + +/** + * Get all available pages from the navigation menu configuration + * Used by UI Settings to display available pages for visibility control + */ +export const getAvailablePages = (): PageMetadata[] => { + const pages: PageMetadata[] = []; + + menuGroups.forEach((group) => { + group.items.forEach((item) => { + // Add top-level items (skip parent containers like 'tools', 'experimental', 'settings') + if (item.page && item.page !== "tools" && item.page !== "experimental" && item.page !== "settings") { + const label = typeof item.label === "string" ? item.label : item.key; + pages.push({ + page: item.page, + label: label, + group: group.groupLabel, + description: pageDescriptions[item.page] || "No description available", + }); + } + + // Add children items + if (item.children) { + const parentLabel = typeof item.label === "string" ? item.label : item.key; + item.children.forEach((child) => { + const childLabel = typeof child.label === "string" ? child.label : child.key; + pages.push({ + page: child.page, + label: childLabel, + group: `${group.groupLabel} > ${parentLabel}`, + description: pageDescriptions[child.page] || "No description available", + }); + }); + } + }); + }); + + return pages; +};