refactor(ui): consolidate dashboard to one shell in the (dashboard) layout (#30166)

* refactor(ui): consolidate dashboard to one shell in the (dashboard) layout

Moves the legacy ?page= switch page into the (dashboard) route group and
hoists Navbar, sidebar, ThemeProvider, and DebugWarningBanner into the
shared layout with real props, deleting the degraded duplicate shell that
wrapped migrated routes. The active page key now derives from the URL at
render time, so navigating between legacy and migrated pages no longer
remounts the shell.

useProxySettings becomes a React Query hook taking accessToken, shared by
the navbar, the AdminPanel arm, and migrated pages; this replaces the
lifted proxySettings state and the Navbar setProxySettings prop drilling.
The invitation onboarding flow (?invitation_id=) keeps rendering without
chrome via a layout escape hatch. Dead dark mode state and the no-op antd
ConfigProvider are removed.

* fix(ui): include accessToken in useProxySettings query key

The queryFn closes over accessToken, so the key must include it for the
cache to be honest about its inputs. Settings are instance-global today,
which made the omission harmless, but a token change while mounted would
have served the cached entry without refetching.

* test(ui): point CreateKeyPage test at the moved page

The page moved into the (dashboard) route group and no longer renders
the navbar (the layout owns chrome now), so the valid-token test asserts
the default page content (UserDashboard stub) instead.
This commit is contained in:
ryan-crabbe-berri 2026-06-10 18:37:44 -07:00 committed by GitHub
parent 496f5b9859
commit 4def6916da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 256 additions and 396 deletions

View File

@ -164,11 +164,6 @@
"count": 2
}
},
"src/app/(dashboard)/layout.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx": {
"no-restricted-imports": {
"count": 1
@ -228,11 +223,6 @@
"count": 1
}
},
"src/app/page.tsx": {
"unused-imports/no-unused-imports": {
"count": 2
}
},
"src/components/AIHub/AgentHubTableColumns.test.tsx": {
"unused-imports/no-unused-imports": {
"count": 1
@ -243,14 +233,6 @@
"count": 1
}
},
"src/components/AIHub/ClaudeCodeMarketplaceTab.tsx": {
"no-restricted-imports": {
"count": 1
},
"react-hooks/immutability": {
"count": 1
}
},
"src/components/AIHub/ModelHubTable.test.tsx": {
"max-params": {
"count": 1
@ -303,11 +285,6 @@
"count": 1
}
},
"src/components/AIHub/marketplace_table_columns.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/AdminPanel.tsx": {
"no-restricted-imports": {
"count": 1
@ -816,11 +793,6 @@
"count": 1
}
},
"src/components/agents/agent_table.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/alerting/dynamic_form.tsx": {
"no-restricted-imports": {
"count": 1
@ -956,14 +928,6 @@
"count": 1
}
},
"src/components/claude_code_plugins/plugin_info.tsx": {
"no-restricted-imports": {
"count": 1
},
"react-hooks/immutability": {
"count": 1
}
},
"src/components/claude_code_plugins/plugin_table.tsx": {
"no-restricted-imports": {
"count": 1
@ -1333,14 +1297,6 @@
"count": 2
}
},
"src/components/mcp_tools/mcp_server_columns.tsx": {
"max-params": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
},
"src/components/mcp_tools/mcp_server_cost_config.tsx": {
"no-restricted-imports": {
"count": 1
@ -1489,11 +1445,6 @@
"count": 1
}
},
"src/components/navbar.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/components/networking.tsx": {
"max-params": {
"count": 23

View File

@ -1,10 +1,12 @@
"use client";
import APIReferenceView from "@/app/(dashboard)/api-reference/APIReferenceView";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useProxySettings from "@/app/(dashboard)/hooks/proxySettings/useProxySettings";
const APIReferencePage = () => {
const proxySettings = useProxySettings();
const { accessToken } = useAuthorized();
const proxySettings = useProxySettings(accessToken);
return <APIReferenceView proxySettings={proxySettings} />;
};

View File

@ -1,21 +1,26 @@
import { useState, useEffect } from "react";
import { fetchProxySettings } from "@/utils/proxyUtils";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useQuery } from "@tanstack/react-query";
import { createQueryKeys } from "../common/queryKeysFactory";
export default function useProxySettings() {
const { accessToken } = useAuthorized();
const [proxySettings, setProxySettings] = useState({
PROXY_BASE_URL: "",
PROXY_LOGOUT_URL: "",
LITELLM_UI_API_DOC_BASE_URL: null as string | null,
});
export const proxySettingsKeys = createQueryKeys("proxySettings");
useEffect(() => {
if (!accessToken) return;
fetchProxySettings(accessToken).then((settings) => {
if (settings) setProxySettings(settings);
});
}, [accessToken]);
return proxySettings;
export interface ProxySettings {
PROXY_BASE_URL: string;
PROXY_LOGOUT_URL: string;
LITELLM_UI_API_DOC_BASE_URL?: string | null;
}
const EMPTY_PROXY_SETTINGS: ProxySettings = {
PROXY_BASE_URL: "",
PROXY_LOGOUT_URL: "",
LITELLM_UI_API_DOC_BASE_URL: null,
};
export default function useProxySettings(accessToken: string | null): ProxySettings {
const { data } = useQuery({
queryKey: [...proxySettingsKeys.all, accessToken],
queryFn: () => fetchProxySettings(accessToken),
enabled: Boolean(accessToken),
});
return data ?? EMPTY_PROXY_SETTINGS;
}

View File

@ -1,62 +1,63 @@
"use client";
import React, { Suspense, useEffect, useState } from "react";
import React, { Suspense, useState } from "react";
import Navbar from "@/components/navbar";
import LoadingScreen from "@/components/common_components/LoadingScreen";
import { ThemeProvider } from "@/contexts/ThemeContext";
import { useAuth } from "@/contexts/AuthContext";
import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { DebugWarningBanner } from "@/components/DebugWarningBanner";
import { MIGRATED_PAGES, migratedHref, legacyPageHref, legacyKeyForPathname } from "@/utils/migratedPages";
function LayoutContent({ children }: { children: React.ReactNode }) {
function DashboardShell({ children }: { children: React.ReactNode }) {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
const { accessToken } = useAuthorized();
const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false);
const [page, setPage] = useState(() => {
return legacyKeyForPathname(pathname) || searchParams.get("page") || "api-keys";
});
const { accessToken } = useAuth();
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const handleSetPage = (newPage: string) => {
const page = legacyKeyForPathname(pathname) || searchParams.get("page") || "api-keys";
const navigateToPage = (newPage: string) => {
const migratedRoute = MIGRATED_PAGES[newPage];
router.push(migratedRoute ? migratedHref(migratedRoute) : legacyPageHref(newPage));
setPage(newPage);
};
useEffect(() => {
setPage(legacyKeyForPathname(pathname) || searchParams.get("page") || "api-keys");
}, [pathname, searchParams]);
return (
<div className="flex flex-col min-h-screen">
<Navbar
accessToken={accessToken}
isPublicPage={false}
sidebarCollapsed={sidebarCollapsed}
onToggleSidebar={() => setSidebarCollapsed((v) => !v)}
/>
<DebugWarningBanner accessToken={accessToken} />
<div className="flex flex-1">
<div className="mt-2">
<SidebarProvider setPage={navigateToPage} defaultSelectedKey={page} sidebarCollapsed={sidebarCollapsed} />
</div>
<main className="flex-1">{children}</main>
</div>
</div>
);
}
const toggleSidebar = () => setSidebarCollapsed((v) => !v);
function LayoutContent({ children }: { children: React.ReactNode }) {
const searchParams = useSearchParams();
const { accessToken } = useAuth();
const isInvitationFlow = Boolean(searchParams.get("invitation_id"));
return (
<ThemeProvider accessToken={""}>
<div className="flex flex-col min-h-screen">
<Navbar
isPublicPage={false}
sidebarCollapsed={sidebarCollapsed}
onToggleSidebar={toggleSidebar}
proxySettings={undefined}
setProxySettings={() => {}}
accessToken={accessToken}
/>
<DebugWarningBanner accessToken={accessToken} />
<div className="flex flex-1 overflow-auto">
<div className="mt-2">
<SidebarProvider setPage={handleSetPage} defaultSelectedKey={page} sidebarCollapsed={sidebarCollapsed} />
</div>
<main className="flex-1">{children}</main>
</div>
</div>
<ThemeProvider accessToken={accessToken}>
{isInvitationFlow ? children : <DashboardShell>{children}</DashboardShell>}
</ThemeProvider>
);
}
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<Suspense fallback={<div className="flex items-center justify-center min-h-screen">Loading...</div>}>
<Suspense fallback={<LoadingScreen />}>
<LayoutContent>{children}</LayoutContent>
</Suspense>
);

View File

@ -1,7 +1,6 @@
"use client";
import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider";
import OldModelDashboard from "@/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView";
import ModelsAndEndpointsView from "@/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView";
import PlaygroundPage from "@/app/(dashboard)/playground/page";
import AdminPanel from "@/components/AdminPanel";
import AgentsPanel from "@/components/agents";
@ -10,6 +9,7 @@ import CacheDashboard from "@/components/cache_dashboard";
import ClaudeCodePluginsPanel from "@/components/claude_code_plugins";
import { teamListCall as v2TeamListCall } from "@/app/(dashboard)/hooks/teams/useTeams";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import useProxySettings from "@/app/(dashboard)/hooks/proxySettings/useProxySettings";
import LoadingScreen from "@/components/common_components/LoadingScreen";
import { CostTrackingSettings } from "@/components/CostTrackingSettings";
import GeneralSettings from "@/components/general_settings";
@ -19,7 +19,6 @@ import PoliciesPanel from "@/components/policies";
import { Team } from "@/components/key_team_helpers/key_list";
import { MCPServers } from "@/components/mcp_tools";
import ModelHubTable from "@/components/AIHub/ModelHubTable";
import Navbar from "@/components/navbar";
import { Organization, proxyBaseUrl, getInProductNudgesCall } from "@/components/networking";
import NewUsagePage from "@/components/UsagePage/components/UsagePageView";
import OldTeams from "@/components/OldTeams";
@ -44,7 +43,6 @@ import { MemoryView } from "@/components/MemoryView";
import WorkflowRuns from "@/components/workflow_runs";
import SpendLogsTable from "@/components/view_logs";
import ViewUserDashboard from "@/components/view_users";
import { ThemeProvider } from "@/contexts/ThemeContext";
import { useAuth } from "@/contexts/AuthContext";
import {
buildLoginUrlWithReturn,
@ -55,16 +53,8 @@ import {
} from "@/utils/returnUrlUtils";
import { isAdminRole } from "@/utils/roles";
import { MIGRATED_PAGES, migratedHref } from "@/utils/migratedPages";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
import { ConfigProvider, theme } from "antd";
interface ProxySettings {
PROXY_BASE_URL: string;
PROXY_LOGOUT_URL: string;
LITELLM_UI_API_DOC_BASE_URL?: string | null;
}
function CreateKeyPageContent() {
const { authLoading, token, userID, userRole, userEmail, accessToken, premiumUser, setUserRole, setUserEmail } =
@ -74,10 +64,7 @@ function CreateKeyPageContent() {
const [keys, setKeys] = useState<null | any[]>([]);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [userModels, setUserModels] = useState<string[]>([]);
const [proxySettings, setProxySettings] = useState<ProxySettings>({
PROXY_BASE_URL: "",
PROXY_LOGOUT_URL: "",
});
const proxySettings = useProxySettings(accessToken);
const router = useRouter();
const searchParams = useSearchParams()!;
@ -96,12 +83,6 @@ function CreateKeyPageContent() {
const [showClaudeCodePrompt, setShowClaudeCodePrompt] = useState(false);
const [showClaudeCodeModal, setShowClaudeCodeModal] = useState(false);
// Dark mode state
const [isDarkMode, setIsDarkMode] = useState(false);
const toggleDarkMode = () => {
setIsDarkMode(!isDarkMode);
};
const invitation_id = searchParams.get("invitation_id");
// Parse URL query parameters for pre-filling the create key form
@ -154,33 +135,11 @@ function CreateKeyPageContent() {
};
}, [searchParams, autoOpenCreate]);
// Get page from URL, default to 'api-keys' if not present
const [page, setPage] = useState(() => {
return searchParams.get("page") || "api-keys";
});
const updatePage = (newPage: string) => {
const migratedRoute = MIGRATED_PAGES[newPage];
if (migratedRoute) {
router.push(migratedHref(migratedRoute));
setPage(newPage);
return;
}
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set("page", newPage);
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
setPage(newPage);
};
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const page = searchParams.get("page") || "api-keys";
// Track if we've already attempted a return URL redirect to prevent race conditions
const hasAttemptedReturnRedirectRef = useRef(false);
const toggleSidebar = () => {
setSidebarCollapsed(!sidebarCollapsed);
};
const addKey = (data: any) => {
setKeys((prevData) => (prevData ? [...prevData, data] : [data]));
setCreateClicked(() => !createClicked);
@ -349,14 +308,26 @@ function CreateKeyPageContent() {
}
return (
<Suspense fallback={<LoadingScreen />}>
<ConfigProvider
theme={{
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
}}
>
<ThemeProvider accessToken={accessToken}>
{invitation_id ? (
<>
{invitation_id ? (
<UserDashboard
userID={userID}
userRole={userRole}
premiumUser={premiumUser}
teams={teams}
keys={keys}
setUserRole={setUserRole}
userEmail={userEmail}
setUserEmail={setUserEmail}
setTeams={setTeams}
setKeys={setKeys}
organizations={organizations}
addKey={addKey}
createClicked={createClicked}
/>
) : (
<>
{page == "api-keys" ? (
<UserDashboard
userID={userID}
userRole={userRole}
@ -371,209 +342,164 @@ function CreateKeyPageContent() {
organizations={organizations}
addKey={addKey}
createClicked={createClicked}
autoOpenCreate={autoOpenCreate}
prefillData={prefillData}
/>
) : (
<div className="flex flex-col min-h-screen">
<Navbar
setProxySettings={setProxySettings}
proxySettings={proxySettings}
) : page == "models" ? (
<ModelsAndEndpointsView
token={token}
keys={keys}
modelData={modelData}
setModelData={setModelData}
premiumUser={premiumUser}
teams={teams}
/>
) : page == "llm-playground" ? (
<PlaygroundPage />
) : page == "users" ? (
<ViewUserDashboard
userID={userID}
userRole={userRole}
token={token}
keys={keys}
teams={teams}
accessToken={accessToken}
setKeys={setKeys}
/>
) : page == "teams" ? (
<OldTeams
teams={teams}
setTeams={setTeams}
accessToken={accessToken}
userID={userID}
userRole={userRole}
organizations={organizations}
premiumUser={premiumUser}
searchParams={searchParams}
/>
) : page == "organizations" ? (
<Organizations
organizations={organizations}
setOrganizations={setOrganizations}
userModels={userModels}
accessToken={accessToken}
userRole={userRole}
premiumUser={premiumUser}
/>
) : page == "admin-panel" ? (
<AdminPanel proxySettings={proxySettings} />
) : page == "logging-and-alerts" ? (
<Settings userID={userID} userRole={userRole} accessToken={accessToken} premiumUser={premiumUser} />
) : page == "budgets" ? (
<BudgetPanel accessToken={accessToken} />
) : page == "guardrails" ? (
<GuardrailsPanel accessToken={accessToken} userRole={userRole} />
) : page == "policies" ? (
<PoliciesPanel accessToken={accessToken} userRole={userRole} />
) : page == "agents" ? (
<AgentsPanel accessToken={accessToken} userRole={userRole} teams={teams} />
) : page == "prompts" ? (
<PromptsPanel accessToken={accessToken} userRole={userRole} />
) : page == "transform-request" ? (
<TransformRequestPanel accessToken={accessToken} />
) : page == "router-settings" ? (
<GeneralSettings userID={userID} userRole={userRole} accessToken={accessToken} modelData={modelData} />
) : page == "ui-theme" ? (
<UIThemeSettings userID={userID} userRole={userRole} accessToken={accessToken} />
) : page == "cost-tracking" ? (
<CostTrackingSettings userID={userID} userRole={userRole} accessToken={accessToken} />
) : page == "model-hub-table" ? (
isAdminRole(userRole) ? (
<ModelHubTable
accessToken={accessToken}
isPublicPage={false}
sidebarCollapsed={sidebarCollapsed}
onToggleSidebar={toggleSidebar}
publicPage={false}
premiumUser={premiumUser}
userRole={userRole}
/>
<div className="flex flex-1">
<div className="mt-2">
<SidebarProvider setPage={updatePage} defaultSelectedKey={page} sidebarCollapsed={sidebarCollapsed} />
</div>
{page == "api-keys" ? (
<UserDashboard
userID={userID}
userRole={userRole}
premiumUser={premiumUser}
teams={teams}
keys={keys}
setUserRole={setUserRole}
userEmail={userEmail}
setUserEmail={setUserEmail}
setTeams={setTeams}
setKeys={setKeys}
organizations={organizations}
addKey={addKey}
createClicked={createClicked}
autoOpenCreate={autoOpenCreate}
prefillData={prefillData}
/>
) : page == "models" ? (
<OldModelDashboard
token={token}
keys={keys}
modelData={modelData}
setModelData={setModelData}
premiumUser={premiumUser}
teams={teams}
/>
) : page == "llm-playground" ? (
<PlaygroundPage />
) : page == "users" ? (
<ViewUserDashboard
userID={userID}
userRole={userRole}
token={token}
keys={keys}
teams={teams}
accessToken={accessToken}
setKeys={setKeys}
/>
) : page == "teams" ? (
<OldTeams
teams={teams}
setTeams={setTeams}
accessToken={accessToken}
userID={userID}
userRole={userRole}
organizations={organizations}
premiumUser={premiumUser}
searchParams={searchParams}
/>
) : page == "organizations" ? (
<Organizations
organizations={organizations}
setOrganizations={setOrganizations}
userModels={userModels}
accessToken={accessToken}
userRole={userRole}
premiumUser={premiumUser}
/>
) : page == "admin-panel" ? (
<AdminPanel proxySettings={proxySettings} />
) : page == "logging-and-alerts" ? (
<Settings userID={userID} userRole={userRole} accessToken={accessToken} premiumUser={premiumUser} />
) : page == "budgets" ? (
<BudgetPanel accessToken={accessToken} />
) : page == "guardrails" ? (
<GuardrailsPanel accessToken={accessToken} userRole={userRole} />
) : page == "policies" ? (
<PoliciesPanel accessToken={accessToken} userRole={userRole} />
) : page == "agents" ? (
<AgentsPanel accessToken={accessToken} userRole={userRole} teams={teams} />
) : page == "prompts" ? (
<PromptsPanel accessToken={accessToken} userRole={userRole} />
) : page == "transform-request" ? (
<TransformRequestPanel accessToken={accessToken} />
) : page == "router-settings" ? (
<GeneralSettings
userID={userID}
userRole={userRole}
accessToken={accessToken}
modelData={modelData}
/>
) : page == "ui-theme" ? (
<UIThemeSettings userID={userID} userRole={userRole} accessToken={accessToken} />
) : page == "cost-tracking" ? (
<CostTrackingSettings userID={userID} userRole={userRole} accessToken={accessToken} />
) : page == "model-hub-table" ? (
isAdminRole(userRole) ? (
<ModelHubTable
accessToken={accessToken}
publicPage={false}
premiumUser={premiumUser}
userRole={userRole}
/>
) : (
<PublicModelHub accessToken={accessToken} isEmbedded={true} />
)
) : page == "caching" ? (
<CacheDashboard
userID={userID}
userRole={userRole}
token={token}
accessToken={accessToken}
premiumUser={premiumUser}
/>
) : page == "pass-through-settings" ? (
<PassThroughSettings
userID={userID}
userRole={userRole}
accessToken={accessToken}
modelData={modelData}
premiumUser={premiumUser}
/>
) : page == "logs" ? (
<SpendLogsTable
userID={userID}
userRole={userRole}
token={token}
accessToken={accessToken}
premiumUser={premiumUser}
/>
) : page == "mcp-servers" ? (
<MCPServers accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "search-tools" ? (
<SearchTools accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "tag-management" ? (
<TagManagement accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "skills" || page == "claude-code-plugins" ? (
<ClaudeCodePluginsPanel accessToken={accessToken} userRole={userRole} />
) : page == "access-groups" ? (
<AccessGroupsPage />
) : page == "projects" ? (
<ProjectsPage />
) : page == "vector-stores" ? (
<VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "tool-policies" ? (
<ToolPoliciesView accessToken={accessToken} userRole={userRole} />
) : page == "workflows" ? (
<WorkflowRuns accessToken={accessToken} />
) : page == "memory" ? (
<MemoryView accessToken={accessToken} userID={userID} userRole={userRole} />
) : page == "guardrails-monitor" ? (
<GuardrailsMonitorView accessToken={accessToken} />
) : page == "new_usage" ? (
<NewUsagePage
teams={(teams as Team[]) ?? []}
organizations={(organizations as Organization[]) ?? []}
/>
) : (
<Usage
userID={userID}
userRole={userRole}
token={token}
accessToken={accessToken}
keys={keys}
premiumUser={premiumUser}
/>
)}
</div>
{/* Survey Components */}
<SurveyPrompt
isVisible={showSurveyPrompt && !nudgesDisabled}
onOpen={handleOpenSurvey}
onDismiss={handleDismissSurveyPrompt}
/>
<SurveyModal
isOpen={showSurveyModal}
onClose={handleSurveyModalClose}
onComplete={handleSurveyComplete}
/>
{/* Claude Code Components */}
<ClaudeCodePrompt
isVisible={showClaudeCodePrompt && !nudgesDisabled}
onOpen={handleOpenClaudeCode}
onDismiss={handleDismissClaudeCodePrompt}
/>
<ClaudeCodeModal
isOpen={showClaudeCodeModal}
onClose={handleClaudeCodeModalClose}
onComplete={handleClaudeCodeComplete}
/>
</div>
) : (
<PublicModelHub accessToken={accessToken} isEmbedded={true} />
)
) : page == "caching" ? (
<CacheDashboard
userID={userID}
userRole={userRole}
token={token}
accessToken={accessToken}
premiumUser={premiumUser}
/>
) : page == "pass-through-settings" ? (
<PassThroughSettings
userID={userID}
userRole={userRole}
accessToken={accessToken}
modelData={modelData}
premiumUser={premiumUser}
/>
) : page == "logs" ? (
<SpendLogsTable
userID={userID}
userRole={userRole}
token={token}
accessToken={accessToken}
premiumUser={premiumUser}
/>
) : page == "mcp-servers" ? (
<MCPServers accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "search-tools" ? (
<SearchTools accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "tag-management" ? (
<TagManagement accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "skills" || page == "claude-code-plugins" ? (
<ClaudeCodePluginsPanel accessToken={accessToken} userRole={userRole} />
) : page == "access-groups" ? (
<AccessGroupsPage />
) : page == "projects" ? (
<ProjectsPage />
) : page == "vector-stores" ? (
<VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "tool-policies" ? (
<ToolPoliciesView accessToken={accessToken} userRole={userRole} />
) : page == "workflows" ? (
<WorkflowRuns accessToken={accessToken} />
) : page == "memory" ? (
<MemoryView accessToken={accessToken} userID={userID} userRole={userRole} />
) : page == "guardrails-monitor" ? (
<GuardrailsMonitorView accessToken={accessToken} />
) : page == "new_usage" ? (
<NewUsagePage teams={(teams as Team[]) ?? []} organizations={(organizations as Organization[]) ?? []} />
) : (
<Usage
userID={userID}
userRole={userRole}
token={token}
accessToken={accessToken}
keys={keys}
premiumUser={premiumUser}
/>
)}
</ThemeProvider>
</ConfigProvider>
</Suspense>
{/* Survey Components */}
<SurveyPrompt
isVisible={showSurveyPrompt && !nudgesDisabled}
onOpen={handleOpenSurvey}
onDismiss={handleDismissSurveyPrompt}
/>
<SurveyModal isOpen={showSurveyModal} onClose={handleSurveyModalClose} onComplete={handleSurveyComplete} />
{/* Claude Code Components */}
<ClaudeCodePrompt
isVisible={showClaudeCodePrompt && !nudgesDisabled}
onOpen={handleOpenClaudeCode}
onDismiss={handleDismissClaudeCodePrompt}
/>
<ClaudeCodeModal
isOpen={showClaudeCodeModal}
onClose={handleClaudeCodeModalClose}
onComplete={handleClaudeCodeComplete}
/>
</>
)}
</>
);
}

View File

@ -72,7 +72,10 @@ vi.mock("./Navbar/UserDropdown/UserDropdown", async (importOriginal) => {
});
vi.mock("@/utils/proxyUtils", () => ({
fetchProxySettings: vi.fn(),
fetchProxySettings: vi.fn().mockResolvedValue({
PROXY_BASE_URL: "",
PROXY_LOGOUT_URL: "https://example.com/logout",
}),
}));
// Mock CommunityEngagementButtons component
@ -137,8 +140,6 @@ Object.defineProperty(window, "location", {
describe("Navbar", () => {
const defaultProps = {
proxySettings: {},
setProxySettings: vi.fn(),
accessToken: "test-token",
isPublicPage: false,
};
@ -298,7 +299,9 @@ describe("Navbar", () => {
const cookieUtils = vi.mocked(await import("@/utils/cookieUtils"));
expect(cookieUtils.clearTokenCookies).toHaveBeenCalled();
expect(window.location.href).toBe("");
await waitFor(() => {
expect(window.location.href).toBe("https://example.com/logout");
});
});
it("should not render dark mode toggle slider", () => {

View File

@ -6,11 +6,11 @@ import { getProxyBaseUrl } from "@/components/networking";
import { useTheme } from "@/contexts/ThemeContext";
import { clearTokenCookies } from "@/utils/cookieUtils";
import { clearStoredReturnUrl } from "@/utils/returnUrlUtils";
import { fetchProxySettings } from "@/utils/proxyUtils";
import useProxySettings from "@/app/(dashboard)/hooks/proxySettings/useProxySettings";
import { DownOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
import { Tag } from "antd";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import React from "react";
import { BlogDropdown } from "./Navbar/BlogDropdown/BlogDropdown";
import { CommunityEngagementButtons } from "./Navbar/CommunityEngagementButtons/CommunityEngagementButtons";
import { NAV_PRODUCT_LINK_CLASS } from "./Navbar/navProductLinkClass";
@ -19,8 +19,6 @@ import UserDropdown from "./Navbar/UserDropdown/UserDropdown";
import WorkerDropdown from "./Navbar/WorkerDropdown/WorkerDropdown";
interface NavbarProps {
proxySettings: any;
setProxySettings: React.Dispatch<React.SetStateAction<any>>;
accessToken: string | null;
isPublicPage: boolean;
sidebarCollapsed?: boolean;
@ -28,15 +26,13 @@ interface NavbarProps {
}
const Navbar: React.FC<NavbarProps> = ({
proxySettings,
setProxySettings,
accessToken,
isPublicPage = false,
sidebarCollapsed = false,
onToggleSidebar,
}) => {
const baseUrl = getProxyBaseUrl();
const [logoutUrl, setLogoutUrl] = useState("");
const proxySettings = useProxySettings(accessToken);
const { logoUrl } = useTheme();
const { data: healthData } = useHealthReadinessDetails(accessToken);
const version = healthData?.litellm_version;
@ -47,29 +43,11 @@ const Navbar: React.FC<NavbarProps> = ({
const imageUrl = logoUrl || `${baseUrl}/get_image`;
useEffect(() => {
const initializeProxySettings = async () => {
if (accessToken) {
const settings = await fetchProxySettings(accessToken);
console.log("response from fetchProxySettings", settings);
if (settings) {
setProxySettings(settings);
}
}
};
initializeProxySettings();
}, [accessToken]);
useEffect(() => {
setLogoutUrl(proxySettings?.PROXY_LOGOUT_URL || "");
}, [proxySettings]);
const handleLogout = () => {
clearTokenCookies();
localStorage.removeItem("litellm_selected_worker_id");
localStorage.removeItem("litellm_worker_url");
window.location.href = logoutUrl;
window.location.href = proxySettings.PROXY_LOGOUT_URL || "";
};
const handleWorkerSwitch = (workerId: string) => {

View File

@ -121,7 +121,6 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
const [selectedModel, setSelectedModel] = useState<null | ModelGroupInfo>(null);
const [selectedAgent, setSelectedAgent] = useState<null | AgentCard>(null);
const [selectedMcpServer, setSelectedMcpServer] = useState<null | MCPServerData>(null);
const [proxySettings, setProxySettings] = useState<any>({});
const [activeTab, setActiveTab] = useState<string>("models");
const [skillHubData, setSkillHubData] = useState<Plugin[]>([]);
const [skillLoading, setSkillLoading] = useState<boolean>(false);
@ -981,14 +980,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
<ThemeProvider accessToken={accessToken}>
<div className={isEmbedded ? "w-full" : "min-h-screen bg-white"}>
{/* Navigation - only show when not embedded */}
{!isEmbedded && (
<Navbar
setProxySettings={setProxySettings}
proxySettings={proxySettings}
accessToken={accessToken || null}
isPublicPage={true}
/>
)}
{!isEmbedded && <Navbar accessToken={accessToken || null} isPublicPage={true} />}
<div className={isEmbedded ? "w-full p-6" : "w-full px-8 py-12"}>
{/* Embedded Explainer - only shown when embedded in dashboard */}

View File

@ -6,6 +6,7 @@ import { PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons"
import { useRoutingGroups, useSaveRoutingGroups } from "@/app/(dashboard)/hooks/routingGroups/useRoutingGroups";
import { useRouterFields } from "@/app/(dashboard)/hooks/router/useRouterFields";
import { useModelHub } from "@/app/(dashboard)/hooks/models/useModels";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useProxySettings from "@/app/(dashboard)/hooks/proxySettings/useProxySettings";
import RoutingGroupsTable from "./RoutingGroupsTable";
import RoutingGroupModal from "./RoutingGroupModal";
@ -18,7 +19,8 @@ const RoutingGroups: React.FC = () => {
const { data, isLoading, refetch, isFetching } = useRoutingGroups();
const { data: routerFields } = useRouterFields();
const { data: modelHub } = useModelHub();
const proxySettings = useProxySettings();
const { accessToken } = useAuthorized();
const proxySettings = useProxySettings(accessToken);
const saveMutation = useSaveRoutingGroups();
const [searchQuery, setSearchQuery] = useState("");

View File

@ -149,7 +149,7 @@ vi.mock("@/lib/cva.config", () => ({
}));
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import CreateKeyPage from "@/app/page";
import CreateKeyPage from "@/app/(dashboard)/page";
import { AuthProvider } from "@/contexts/AuthContext";
// The page consumes auth state via useAuth(). Wrap it so the hook resolves
@ -242,7 +242,7 @@ describe("CreateKeyPage auth behavior", () => {
expect(wroteDeletion).toBe(true);
});
it("does NOT redirect when token is valid and renders the app chrome", async () => {
it("does NOT redirect when token is valid and renders the page content", async () => {
// Arrange: valid token in cookie
setCookie("token=validtoken");
@ -269,9 +269,9 @@ describe("CreateKeyPage auth behavior", () => {
expect(window.location.replace).not.toHaveBeenCalled();
});
// And some top-level UI appears (Navbar stub)
// And the default page content appears (UserDashboard stub; chrome now lives in the layout)
await waitFor(() => {
expect(screen.getByTestId("navbar")).toBeInTheDocument();
expect(screen.getByTestId("user-dashboard")).toBeInTheDocument();
});
});