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:
parent
496f5b9859
commit
4def6916da
@ -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
|
||||
|
||||
@ -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} />;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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("");
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user