diff --git a/ui/litellm-dashboard/.env.development b/ui/litellm-dashboard/.env.development new file mode 100644 index 00000000000..b408125408b --- /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 00000000000..1df897e7d23 --- /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 9de9f2f1368..209cf2e67fd 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 00000000000..822a30e6403 --- /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 8b16074b25d..fce7f691925 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 851ecec6654..ad8a82703f7 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 15dd7139174..82be28e4951 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 65ebd2d5717..b88f8919448 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 00c2e9a1e2b..f08b4ea367d 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 2bee7f72b14..cbe3db4e8ff 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 2445240e1ee..f4718e8ba05 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 50070400004..998b392f774 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 979337f5aad..dd99de0615b 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 badcaf93d9b..66b5520f067 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 a3840bdc624..d266a51667c 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 2277800dad7..377f6d31139 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 73cd433e78e..1fb40b87895 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 cbf10f97715..015d8c43f9e 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 92e5896e764..5f04f8a28a7 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 7dfc959867c..611d92f1a32 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 83804937986..00000000000 --- 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 3815df002bd..8e25dc192c8 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 34916cddb9f..a0d8d162c68 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 d4035e83f09..c37d94abb1a 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 ba04e2644a8..26adaa11dbd 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 3fad7c0a35e..d146d5ba79e 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 d50fdf46a6e..f1914f463a1 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;