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 00000000000..265a6d81954 --- /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 2ebfbc5d4f6..96bb9b5a4bf 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 00000000000..cb7f0f1a32c --- /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 fce7f691925..9eb4191be1d 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 00000000000..0948b7626db --- /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 00000000000..e49bd342c05 --- /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 00000000000..6dcbcdc697c --- /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 00000000000..9521f4f69f1 --- /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 00000000000..0836a03b7e7 --- /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 00000000000..0e686387b34 --- /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 00000000000..50cee215eb9 --- /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 74c0476f199..ebb0c345926 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 00000000000..4c6e7acc199 --- /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 00000000000..86967b660fd --- /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 00000000000..2c523ea4039 --- /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 00000000000..6112fac3161 --- /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 00000000000..a851ce766aa --- /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 00000000000..b13e3c42f9e --- /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 00000000000..2b5463cd81f --- /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 00000000000..c6826cf11df --- /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 00000000000..f1f0460d539 --- /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 00000000000..6324f3c5556 --- /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 00000000000..1bea7ac74a5 --- /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 00000000000..8516a0faa1a --- /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 00000000000..c20b13c67d9 --- /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 00000000000..dd531665741 --- /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 015d8c43f9e..180d3336f1c 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 5f04f8a28a7..7ba7a2fea53 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 f83972e0c18..ceee52399ef 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" ? ( + + ) : ( + + )} +
-
- )} -
-
+ )} + + + ); }