Merge pull request #15236 from BerriAI/litellm_ui_refactor_genesis

LiteLLM UI Refactor Infrastructure
This commit is contained in:
Krish Dholakia
2025-10-08 19:19:02 -07:00
committed by GitHub
43 changed files with 1263 additions and 20 deletions
+2
View File
@@ -0,0 +1,2 @@
NODE_ENV=development
NEXT_PUBLIC_BASE_URL=""
+2
View File
@@ -0,0 +1,2 @@
NODE_ENV=production
NEXT_PUBLIC_BASE_URL="ui/"
@@ -0,0 +1,17 @@
"use client";
import APIRef from "@/components/api_ref";
import { useState } from "react";
interface ProxySettings {
PROXY_BASE_URL: string;
PROXY_LOGOUT_URL: string;
}
const APIReferencePage = () => {
const [proxySettings, setProxySettings] = useState<ProxySettings>({ PROXY_BASE_URL: "", PROXY_LOGOUT_URL: "" });
return <APIRef proxySettings={proxySettings} />;
};
export default APIReferencePage;
@@ -0,0 +1,401 @@
"use client";
import { Layout, Menu, ConfigProvider } from "antd";
import {
KeyOutlined,
PlayCircleOutlined,
BlockOutlined,
BarChartOutlined,
TeamOutlined,
BankOutlined,
UserOutlined,
SettingOutlined,
ApiOutlined,
AppstoreOutlined,
DatabaseOutlined,
FileTextOutlined,
LineChartOutlined,
SafetyOutlined,
ExperimentOutlined,
ToolOutlined,
TagsOutlined,
} from "@ant-design/icons";
// import {
// all_admin_roles,
// rolesWithWriteAccess,
// internalUserRoles,
// isAdminRole,
// } from "../utils/roles";
// import UsageIndicator from "./usage_indicator";
import * as React from "react";
import { useRouter, usePathname } from "next/navigation";
import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "@/utils/roles";
import UsageIndicator from "@/components/usage_indicator";
const { Sider } = Layout;
// -------- Types --------
interface SidebarProps {
accessToken: string | null;
userRole: string;
/** Fallback selection id (legacy), used if path can't be matched */
defaultSelectedKey: string;
collapsed?: boolean;
}
interface MenuItemCfg {
key: string;
page: string; // legacy id; we map this to a path below
label: string;
roles?: string[];
children?: MenuItemCfg[];
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
};
/** Map legacy `page` ids to real app routes (relative, no leading slash). */
const routeFor = (slug: string): string => {
switch (slug) {
// top level
case "api-keys":
return "virtual-keys";
case "llm-playground":
return "test-key";
case "models":
return "models-and-endpoints";
case "new_usage":
return "usage";
case "teams":
return "teams";
case "organizations":
return "organizations";
case "users":
return "users";
case "api_ref":
return "api-reference";
case "model-hub-table":
// If you intend the newer in-dashboard page, use "model-hub".
return "model-hub";
case "logs":
return "logs";
case "guardrails":
return "guardrails";
// tools
case "mcp-servers":
return "tools/mcp-servers";
case "vector-stores":
return "tools/vector-stores";
// experimental
case "caching":
return "experimental/caching";
case "prompts":
return "experimental/prompts";
case "budgets":
return "experimental/budgets";
case "transform-request":
return "experimental/api-playground";
case "tag-management":
return "experimental/tag-management";
case "usage": // "Old Usage"
return "experimental/old-usage";
// settings
case "general-settings":
return "settings/router-settings";
case "settings": // "Logging & Alerts"
return "settings/logging-and-alerts";
case "admin-panel":
return "settings/admin-settings";
case "ui-theme":
return "settings/ui-theme";
default:
// treat as already a relative path
return slug.replace(/^\/+/, "");
}
};
/** Prefix base path ("/" or "/ui/") */
const toHref = (slugOrPath: string) => {
const base = getBasePath(); // "/" or "/ui/"
const rel = routeFor(slugOrPath).replace(/^\/+|\/+$/g, "");
return `${base}${rel}`;
};
const Sidebar2: React.FC<SidebarProps> = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => {
const router = useRouter();
const pathname = usePathname() || "/";
// ----- Menu config (unchanged labels/icons; same appearance) -----
const menuItems: MenuItemCfg[] = [
{ key: "1", page: "api-keys", label: "Virtual Keys", icon: <KeyOutlined style={{ fontSize: 18 }} /> },
{
key: "3",
page: "llm-playground",
label: "Test Key",
icon: <PlayCircleOutlined style={{ fontSize: 18 }} />,
roles: rolesWithWriteAccess,
},
{
key: "2",
page: "models",
label: "Models + Endpoints",
icon: <BlockOutlined style={{ fontSize: 18 }} />,
roles: rolesWithWriteAccess,
},
{
key: "12",
page: "new_usage",
label: "Usage",
icon: <BarChartOutlined style={{ fontSize: 18 }} />,
roles: [...all_admin_roles, ...internalUserRoles],
},
{ key: "6", page: "teams", label: "Teams", icon: <TeamOutlined style={{ fontSize: 18 }} /> },
{
key: "17",
page: "organizations",
label: "Organizations",
icon: <BankOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "5",
page: "users",
label: "Internal Users",
icon: <UserOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{ key: "14", page: "api_ref", label: "API Reference", icon: <ApiOutlined style={{ fontSize: 18 }} /> },
{
key: "16",
page: "model-hub-table",
label: "Model Hub",
icon: <AppstoreOutlined style={{ fontSize: 18 }} />,
},
{ key: "15", page: "logs", label: "Logs", icon: <LineChartOutlined style={{ fontSize: 18 }} /> },
{
key: "11",
page: "guardrails",
label: "Guardrails",
icon: <SafetyOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "26",
page: "tools",
label: "Tools",
icon: <ToolOutlined style={{ fontSize: 18 }} />,
children: [
{ key: "18", page: "mcp-servers", label: "MCP Servers", icon: <ToolOutlined style={{ fontSize: 18 }} /> },
{
key: "21",
page: "vector-stores",
label: "Vector Stores",
icon: <DatabaseOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
],
},
{
key: "experimental",
page: "experimental",
label: "Experimental",
icon: <ExperimentOutlined style={{ fontSize: 18 }} />,
children: [
{
key: "9",
page: "caching",
label: "Caching",
icon: <DatabaseOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "25",
page: "prompts",
label: "Prompts",
icon: <FileTextOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "10",
page: "budgets",
label: "Budgets",
icon: <BankOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "20",
page: "transform-request",
label: "API Playground",
icon: <ApiOutlined style={{ fontSize: 18 }} />,
roles: [...all_admin_roles, ...internalUserRoles],
},
{
key: "19",
page: "tag-management",
label: "Tag Management",
icon: <TagsOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{ key: "4", page: "usage", label: "Old Usage", icon: <BarChartOutlined style={{ fontSize: 18 }} /> },
],
},
{
key: "settings",
page: "settings",
label: "Settings",
icon: <SettingOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
children: [
{
key: "11",
page: "general-settings",
label: "Router Settings",
icon: <SettingOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "8",
page: "settings",
label: "Logging & Alerts",
icon: <SettingOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "13",
page: "admin-panel",
label: "Admin Settings",
icon: <SettingOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "14",
page: "ui-theme",
label: "UI Theme",
icon: <SettingOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
],
},
];
// ----- Filter by role without mutating originals -----
const filteredMenuItems = React.useMemo<MenuItemCfg[]>(() => {
return menuItems
.filter((item) => !item.roles || item.roles.includes(userRole))
.map((item) => ({
...item,
children: item.children ? item.children.filter((c) => !c.roles || c.roles.includes(userRole)) : undefined,
}));
}, [userRole]);
// ----- Compute selected key from current path -----
const selectedMenuKey = React.useMemo(() => {
const base = getBasePath();
// strip base prefix and leading slash -> "virtual-keys", "tools/mcp-servers", etc.
const rel = pathname.startsWith(base) ? pathname.slice(base.length) : pathname.replace(/^\/+/, "");
const relLower = rel.toLowerCase();
const matchesPath = (slug: string) => {
const route = routeFor(slug).toLowerCase();
return relLower === route || relLower.startsWith(`${route}/`);
};
// search top-level
for (const item of filteredMenuItems) {
if (!item.children && matchesPath(item.page)) return item.key;
if (item.children) {
for (const child of item.children) {
if (matchesPath(child.page)) return child.key;
}
}
}
// fallback to legacy defaultSelectedKey mapping
const fallback = filteredMenuItems.find((i) => i.page === defaultSelectedKey)?.key;
if (fallback) return fallback;
for (const item of filteredMenuItems) {
if (item.children?.some((c) => c.page === defaultSelectedKey)) {
const child = item.children.find((c) => c.page === defaultSelectedKey)!;
return child.key;
}
}
return "1";
}, [pathname, filteredMenuItems, defaultSelectedKey]);
// ----- Navigation -----
const goTo = (slug: string) => {
const href = toHref(slug);
router.push(href);
};
return (
<Layout style={{ minHeight: "100vh" }}>
<Sider
theme="light"
width={220}
collapsed={collapsed}
collapsedWidth={80}
collapsible
trigger={null}
style={{
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
position: "relative",
}}
>
<ConfigProvider
theme={{
components: {
Menu: {
iconSize: 18,
fontSize: 14,
},
},
}}
>
<Menu
mode="inline"
selectedKeys={[selectedMenuKey]}
defaultOpenKeys={collapsed ? [] : ["llm-tools"]} // kept to preserve original appearance
inlineCollapsed={collapsed}
className="custom-sidebar-menu"
style={{
borderRight: 0,
backgroundColor: "transparent",
fontSize: "14px",
}}
items={filteredMenuItems.map((item) => ({
key: item.key,
icon: item.icon,
label: item.label,
children: item.children?.map((child) => ({
key: child.key,
icon: child.icon,
label: child.label,
onClick: () => goTo(child.page),
})),
onClick: !item.children ? () => goTo(item.page) : undefined,
}))}
/>
</ConfigProvider>
{isAdminRole(userRole) && !collapsed && <UsageIndicator accessToken={accessToken} width={220} />}
</Sider>
</Layout>
);
};
export default Sidebar2;
@@ -0,0 +1,29 @@
import useFeatureFlags from "@/hooks/useFeatureFlags";
import Sidebar from "@/components/leftnav";
import Sidebar2 from "@/app/(dashboard)/components/Sidebar2";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
interface SidebarProviderProps {
defaultSelectedKey: string;
setPage: (newPage: string) => void;
sidebarCollapsed: boolean;
}
const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: SidebarProviderProps) => {
const { refactoredUIFlag } = useFeatureFlags();
const { accessToken, userRole } = useAuthorized();
return refactoredUIFlag ? (
<Sidebar2 accessToken={accessToken} defaultSelectedKey={defaultSelectedKey} userRole={userRole} />
) : (
<Sidebar
accessToken={accessToken}
setPage={setPage}
userRole={userRole}
defaultSelectedKey={defaultSelectedKey}
collapsed={sidebarCollapsed}
/>
);
};
export default SidebarProvider;
@@ -0,0 +1,12 @@
"use client";
import TransformRequestPanel from "@/components/transform_request";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const APIPlaygroundPage = () => {
const { accessToken } = useAuthorized();
return <TransformRequestPanel accessToken={accessToken} />;
};
export default APIPlaygroundPage;
@@ -0,0 +1,12 @@
"use client";
import BudgetPanel from "@/components/budgets/budget_panel";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const BudgetsPage = () => {
const { accessToken } = useAuthorized();
return <BudgetPanel accessToken={accessToken} />;
};
export default BudgetsPage;
@@ -0,0 +1,20 @@
"use client";
import CacheDashboard from "@/components/cache_dashboard";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const CachingPage = () => {
const { token, accessToken, userRole, userId, premiumUser } = useAuthorized();
return (
<CacheDashboard
accessToken={accessToken}
token={token}
userRole={userRole}
userID={userId}
premiumUser={premiumUser}
/>
);
};
export default CachingPage;
@@ -0,0 +1,23 @@
"use client";
import Usage from "@/components/usage";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useState } from "react";
const OldUsagePage = () => {
const { accessToken, token, userRole, userId, premiumUser } = useAuthorized();
const [keys, setKeys] = useState<null | any[]>([]);
return (
<Usage
accessToken={accessToken}
token={token}
userRole={userRole}
userID={userId}
keys={keys}
premiumUser={premiumUser}
/>
);
};
export default OldUsagePage;
@@ -0,0 +1,12 @@
"use client";
import PromptsPanel from "@/components/prompts";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const PromptsPage = () => {
const { accessToken } = useAuthorized();
return <PromptsPanel accessToken={accessToken} />;
};
export default PromptsPage;
@@ -0,0 +1,12 @@
"use client";
import TagManagement from "@/components/tag_management";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const TagManagementPage = () => {
const { accessToken, userId, userRole } = useAuthorized();
return <TagManagement accessToken={accessToken} userID={userId} userRole={userRole} />;
};
export default TagManagementPage;
@@ -0,0 +1,12 @@
"use client";
import GuardrailsPanel from "@/components/guardrails";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const GuardrailsPage = () => {
const { accessToken } = useAuthorized();
return <GuardrailsPanel accessToken={accessToken} />;
};
export default GuardrailsPage;
@@ -0,0 +1,45 @@
"use client";
import { useEffect, useMemo } from "react";
import { useRouter } from "next/navigation";
import { jwtDecode } from "jwt-decode";
import { clearTokenCookies, getCookie } from "@/utils/cookieUtils";
const useAuthorized = () => {
const router = useRouter();
const token = typeof document !== "undefined" ? getCookie("token") : null;
// Redirect after mount if missing/invalid token
useEffect(() => {
if (!token) {
router.replace("/sso/key/generate");
}
}, [token, router]);
// Decode safely
const decoded = useMemo(() => {
if (!token) return null;
try {
return jwtDecode(token) as Record<string, any>;
} catch {
// Bad token in cookie — clear and bounce
clearTokenCookies();
router.replace("/sso/key/generate");
return null;
}
}, [token, router]);
return {
token: token,
accessToken: decoded?.key ?? null,
userId: decoded?.user_id ?? null,
userEmail: decoded?.user_email ?? null,
userRole: decoded?.user_role ?? null,
premiumUser: decoded?.premium_user ?? null,
disabledPersonalKeyCreation: decoded?.disabled_non_admin_personal_key_creation ?? null,
showSSOBanner: decoded?.login_method === "username_password" ?? false,
};
};
export default useAuthorized;
@@ -0,0 +1,20 @@
import { useEffect, useState } from "react";
import { Team } from "@/components/key_team_helpers/key_list";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { fetchTeams } from "@/app/(dashboard)/networking";
const useTeams = () => {
const [teams, setTeams] = useState<Team[] | null>([]);
const { accessToken, userId: userID, userRole } = useAuthorized();
useEffect(() => {
(async () => {
const fetched = await fetchTeams(accessToken, userID, userRole, null);
setTeams(fetched);
})();
}, [accessToken, userID, userRole]);
return { teams, setTeams };
};
export default useTeams;
@@ -0,0 +1,73 @@
"use client";
import React, { useEffect, useState } 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 { useRouter, useSearchParams } from "next/navigation";
/** ---- BASE URL HELPERS ---- */
function normalizeBasePrefix(raw: string | undefined | null): string {
const trimmed = (raw ?? "").trim();
if (!trimmed) return "";
const core = trimmed.replace(/^\/+/, "").replace(/\/+$/, "");
return core ? `/${core}/` : "/";
}
const BASE_PREFIX = normalizeBasePrefix(process.env.NEXT_PUBLIC_BASE_URL);
function withBase(path: string): string {
const body = path.startsWith("/") ? path.slice(1) : path;
const combined = `${BASE_PREFIX}${body}`;
return combined.startsWith("/") ? combined : `/${combined}`;
}
/** -------------------------------- */
export default function Layout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const searchParams = useSearchParams();
const { accessToken, userRole } = useAuthorized();
const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false);
const [page, setPage] = useState(() => {
return searchParams.get("page") || "api-keys";
});
const updatePage = (newPage: string) => {
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set("page", newPage);
router.push(withBase(`/?${newSearchParams.toString()}`)); // always under BASE
setPage(newPage);
};
useEffect(() => {
setPage(searchParams.get("page") || "api-keys");
}, [searchParams]);
const toggleSidebar = () => setSidebarCollapsed((v) => !v);
return (
<ThemeProvider accessToken={""}>
<div className="flex flex-col min-h-screen">
<Navbar
isPublicPage={false}
sidebarCollapsed={sidebarCollapsed}
onToggleSidebar={toggleSidebar}
userID={null}
userEmail={null}
userRole={null}
premiumUser={false}
proxySettings={undefined}
setProxySettings={function (value: any): void {
throw new Error("Function not implemented.");
}}
accessToken={null}
/>
<div className="flex flex-1 overflow-auto">
<div className="mt-2">
<Sidebar2 defaultSelectedKey={page} accessToken={accessToken} userRole={userRole} />
</div>
<main className="flex-1">{children}</main>
</div>
</div>
</ThemeProvider>
);
}
@@ -0,0 +1,28 @@
"use client";
import SpendLogsTable from "@/components/view_logs";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const LogsPage = () => {
const { accessToken, token, userRole, userId, premiumUser } = useAuthorized();
const { teams } = useTeams();
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<SpendLogsTable
accessToken={accessToken}
token={token}
userRole={userRole}
userID={userId}
allTeams={teams || []}
premiumUser={premiumUser}
/>
</QueryClientProvider>
);
};
export default LogsPage;
@@ -0,0 +1,12 @@
"use client";
import ModelHubTable from "@/components/model_hub_table";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const ModelHubPage = () => {
const { accessToken, premiumUser, userRole } = useAuthorized();
return <ModelHubTable accessToken={accessToken} publicPage={false} premiumUser={premiumUser} userRole={userRole} />;
};
export default ModelHubPage;
@@ -0,0 +1,29 @@
"use client";
import ModelDashboard from "@/components/templates/model_dashboard";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { useState } from "react";
const ModelsAndEndpointsPage = () => {
const { token, accessToken, userRole, userId, premiumUser } = useAuthorized();
const [keys, setKeys] = useState<null | any[]>([]);
const { teams } = useTeams();
return (
<ModelDashboard
accessToken={accessToken}
token={token}
userRole={userRole}
userID={userId}
modelData={{ data: [] }}
keys={keys}
setModelData={() => {}}
premiumUser={premiumUser}
teams={teams}
/>
);
};
export default ModelsAndEndpointsPage;
@@ -0,0 +1,17 @@
import { Organization, teamListCall } from "@/components/networking";
export const fetchTeams = async (
accessToken: string,
userID: string | null,
userRole: string | null,
currentOrg: Organization | null,
) => {
let givenTeams;
if (userRole != "Admin" && userRole != "Admin Viewer") {
givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || null, userID);
} else {
givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || null);
}
return givenTeams;
};
@@ -0,0 +1,34 @@
"use client";
import Organizations, { fetchOrganizations } from "@/components/organizations";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useEffect, useState } from "react";
import { Organization } from "@/components/networking";
import { fetchUserModels } from "@/components/organisms/create_key_button";
const OrganizationsPage = () => {
const { userId: userID, accessToken, userRole, premiumUser } = useAuthorized();
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [userModels, setUserModels] = useState<string[]>([]);
useEffect(() => {
fetchOrganizations(accessToken, setOrganizations).then(() => {});
}, [accessToken]);
useEffect(() => {
fetchUserModels(userID, userRole, accessToken, setUserModels).then(() => {});
}, [userID, userRole, accessToken]);
return (
<Organizations
organizations={organizations}
userRole={userRole}
userModels={userModels}
accessToken={accessToken}
setOrganizations={setOrganizations}
premiumUser={premiumUser}
/>
);
};
export default OrganizationsPage;
@@ -0,0 +1,29 @@
"use client";
import AdminPanel from "@/components/admins";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useState } from "react";
import { Team } from "@/components/key_team_helpers/key_list";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
const AdminSettings = () => {
const { teams, setTeams } = useTeams();
const [searchParams, setSearchParams] = useState<URLSearchParams>(() =>
typeof window === "undefined" ? new URLSearchParams() : new URLSearchParams(window.location.search),
);
const { accessToken, userId, premiumUser, showSSOBanner } = useAuthorized();
return (
<AdminPanel
searchParams={searchParams}
accessToken={accessToken}
userID={userId}
setTeams={setTeams}
showSSOBanner={showSSOBanner}
premiumUser={premiumUser}
/>
);
};
export default AdminSettings;
@@ -0,0 +1,12 @@
"use client";
import Settings from "@/components/settings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const LoggingAndAlertsPage = () => {
const { accessToken, userRole, userId, premiumUser } = useAuthorized();
return <Settings accessToken={accessToken} userRole={userRole} userID={userId} premiumUser={premiumUser} />;
};
export default LoggingAndAlertsPage;
@@ -0,0 +1,12 @@
"use client";
import GeneralSettings from "@/components/general_settings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const RouterSettingsPage = () => {
const { accessToken, userRole, userId } = useAuthorized();
return <GeneralSettings accessToken={accessToken} userRole={userRole} userID={userId} modelData={{}} />;
};
export default RouterSettingsPage;
@@ -0,0 +1,12 @@
"use client";
import UIThemeSettings from "@/components/ui_theme_settings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const UIThemePage = () => {
const { userId, userRole, accessToken } = useAuthorized();
return <UIThemeSettings userID={userId} userRole={userRole} accessToken={accessToken} />;
};
export default UIThemePage;
@@ -0,0 +1,35 @@
"use client";
import Teams from "@/components/teams";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { useEffect, useState } from "react";
import { Organization } from "@/components/networking";
import { fetchOrganizations } from "@/components/organizations";
const TeamsPage = () => {
const { accessToken, userId, userRole } = useAuthorized();
const { teams, setTeams } = useTeams();
const [searchParams, setSearchParams] = useState<URLSearchParams>(() =>
typeof window === "undefined" ? new URLSearchParams() : new URLSearchParams(window.location.search),
);
const [organizations, setOrganizations] = useState<Organization[]>([]);
useEffect(() => {
fetchOrganizations(accessToken, setOrganizations).then(() => {});
}, [accessToken]);
return (
<Teams
teams={teams}
searchParams={searchParams}
accessToken={accessToken}
setTeams={setTeams}
userID={userId}
userRole={userRole}
organizations={organizations}
/>
);
};
export default TeamsPage;
@@ -0,0 +1,20 @@
"use client";
import ChatUI from "@/components/chat_ui/ChatUI";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const TestKeyPage = () => {
const { token, accessToken, userRole, userId, disabledPersonalKeyCreation } = useAuthorized();
return (
<ChatUI
accessToken={accessToken}
token={token}
userRole={userRole}
userID={userId}
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
/>
);
};
export default TestKeyPage;
@@ -0,0 +1,19 @@
"use client";
import { MCPServers } from "@/components/mcp_tools";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const MCPServersPage = () => {
const { accessToken, userRole, userId } = useAuthorized();
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<MCPServers accessToken={accessToken} userRole={userRole} userID={userId} />
</QueryClientProvider>
);
};
export default MCPServersPage;
@@ -0,0 +1,12 @@
"use client";
import VectorStoreManagement from "@/components/vector_store_management";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const VectorStoresPage = () => {
const { accessToken, userId, userRole } = useAuthorized();
return <VectorStoreManagement accessToken={accessToken} userID={userId} userRole={userRole} />;
};
export default VectorStoresPage;
@@ -0,0 +1,22 @@
"use client";
import NewUsagePage from "@/components/new_usage";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
const UsagePage = () => {
const { accessToken, userRole, userId, premiumUser } = useAuthorized();
const { teams } = useTeams();
return (
<NewUsagePage
accessToken={accessToken}
userRole={userRole}
userID={userId}
teams={teams ?? []}
premiumUser={premiumUser}
/>
);
};
export default UsagePage;
@@ -0,0 +1,31 @@
"use client";
import ViewUserDashboard from "@/components/view_users";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const UsersPage = () => {
const { accessToken, userRole, userId, token } = useAuthorized();
const [keys, setKeys] = useState<null | any[]>([]);
const { teams } = useTeams();
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<ViewUserDashboard
accessToken={accessToken}
token={token}
keys={keys}
userRole={userRole}
userID={userId}
teams={teams as any}
setKeys={setKeys}
/>
</QueryClientProvider>
);
};
export default UsersPage;
@@ -0,0 +1,52 @@
"use client";
import { useState } from "react";
import useKeyList, { KeyResponse } from "@/components/key_team_helpers/key_list";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import UserDashboard from "@/components/user_dashboard";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { Organization } from "@/components/networking";
const VirtualKeysPage = () => {
const { accessToken, userRole, userId, premiumUser, userEmail } = useAuthorized();
const { teams, setTeams } = useTeams();
const [createClicked, setCreateClicked] = useState<boolean>(false);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const queryClient = new QueryClient();
const { keys, isLoading, error, pagination, refresh, setKeys } = useKeyList({
selectedKeyAlias: null,
currentOrg: null,
accessToken: accessToken || "",
createClicked,
});
const addKey = (data: any) => {
setKeys((prevData) => (prevData ? [...prevData, data] : [data]));
setCreateClicked(() => !createClicked);
};
return (
<QueryClientProvider client={queryClient}>
<UserDashboard
userID={userId}
userRole={userRole}
userEmail={userEmail}
teams={teams}
keys={keys}
setUserRole={() => {}}
setUserEmail={() => {}}
setTeams={setTeams}
setKeys={setKeys}
premiumUser={premiumUser}
organizations={organizations}
addKey={addKey}
createClicked={createClicked}
/>
</QueryClientProvider>
);
};
export default VirtualKeysPage;
+7 -1
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { FeatureFlagsProvider } from "@/hooks/useFeatureFlags";
const inter = Inter({ subsets: ["latin"] });
@@ -17,7 +18,12 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body className={inter.className}>
<FeatureFlagsProvider>
{children}
</FeatureFlagsProvider>
</body>
</html>
);
}
+4 -8
View File
@@ -23,7 +23,6 @@ import ModelHubTable from "@/components/model_hub_table";
import NewUsagePage from "@/components/new_usage";
import APIRef from "@/components/api_ref";
import ChatUI from "@/components/chat_ui/ChatUI";
import Sidebar from "@/components/leftnav";
import Usage from "@/components/usage";
import CacheDashboard from "@/components/cache_dashboard";
import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking";
@@ -39,6 +38,8 @@ import VectorStoreManagement from "@/components/vector_store_management";
import UIThemeSettings from "@/components/ui_theme_settings";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { cx } from "@/lib/cva.config";
import useFeatureFlags from "@/hooks/useFeatureFlags";
import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider";
function getCookie(name: string) {
// Safer cookie read + decoding; handles '=' inside values
@@ -140,6 +141,7 @@ export default function CreateKeyPage() {
const [createClicked, setCreateClicked] = useState<boolean>(false);
const [authLoading, setAuthLoading] = useState(true);
const [userID, setUserID] = useState<string | null>(null);
const { refactoredUIFlag } = useFeatureFlags();
const invitation_id = searchParams.get("invitation_id");
@@ -324,13 +326,7 @@ export default function CreateKeyPage() {
/>
<div className="flex flex-1 overflow-auto">
<div className="mt-2">
<Sidebar
accessToken={accessToken}
setPage={updatePage}
userRole={userRole}
defaultSelectedKey={page}
collapsed={sidebarCollapsed}
/>
<SidebarProvider setPage={updatePage} defaultSelectedKey={page} sidebarCollapsed={sidebarCollapsed} />
</div>
{page == "api-keys" ? (
+18 -2
View File
@@ -1,7 +1,7 @@
import Link from "next/link";
import React, { useState, useEffect } from "react";
import type { MenuProps } from "antd";
import { Dropdown, Tooltip } from "antd";
import { Dropdown, Tooltip, Switch } from "antd";
import { getProxyBaseUrl, Organization } from "@/components/networking";
import { defaultOrg } from "@/components/common_components/default_org";
import {
@@ -19,6 +19,7 @@ import { clearTokenCookies } from "@/utils/cookieUtils";
import { fetchProxySettings } from "@/utils/proxyUtils";
import { useTheme } from "@/contexts/ThemeContext";
import { clearMCPAuthTokens } from "./mcp_tools/mcp_auth_storage";
import useFeatureFlags from "@/hooks/useFeatureFlags";
interface NavbarProps {
userID: string | null;
@@ -43,11 +44,12 @@ const Navbar: React.FC<NavbarProps> = ({
accessToken,
isPublicPage = false,
sidebarCollapsed = false,
onToggleSidebar,
onToggleSidebar
}) => {
const baseUrl = getProxyBaseUrl();
const [logoutUrl, setLogoutUrl] = useState("");
const { logoUrl } = useTheme();
const { refactoredUIFlag, setRefactoredUIFlag } = useFeatureFlags();
// Simple logo URL: use custom logo if available, otherwise default
const imageUrl = logoUrl || `${baseUrl}/get_image`;
@@ -79,6 +81,8 @@ const Navbar: React.FC<NavbarProps> = ({
const userItems: MenuProps["items"] = [
{
key: "user-info",
// Prevent dropdown from closing when interacting with the toggle
onClick: (info) => info.domEvent?.stopPropagation(),
label: (
<div className="px-3 py-3 border-b border-gray-100">
<div className="flex items-center justify-between mb-3">
@@ -115,6 +119,18 @@ const Navbar: React.FC<NavbarProps> = ({
{userEmail || "Unknown"}
</span>
</div>
{/* NEW: Feature flag label + toggle below the email field */}
<div className="flex items-center text-sm pt-2 mt-2 border-t border-gray-100">
<span className="text-gray-500 text-xs">Refactored UI</span>
<Switch
className="ml-auto"
size="small"
checked={refactoredUIFlag}
onChange={(checked) => setRefactoredUIFlag(checked)}
aria-label="Toggle refactored UI feature flag"
/>
</div>
</div>
</div>
),
@@ -34,18 +34,34 @@ if (isLocal != true) {
console.log = function () {};
}
const getWindowLocation = () => {
if (typeof window === "undefined") {
return null;
}
return window.location;
};
const updateProxyBaseUrl = (serverRootPath: string, receivedProxyBaseUrl: string | null = null) => {
/**
* Special function for updating the proxy base url. Should only be called by getUiConfig.
*/
const defaultProxyBaseUrl = isLocal ? "http://localhost:4000" : window.location.origin;
let initialProxyBaseUrl = receivedProxyBaseUrl || defaultProxyBaseUrl;
const browserLocation = getWindowLocation();
const resolvedDefaultProxyBaseUrl = isLocal ? "http://localhost:4000" : browserLocation?.origin ?? null;
let initialProxyBaseUrl = receivedProxyBaseUrl || resolvedDefaultProxyBaseUrl;
console.log("proxyBaseUrl:", proxyBaseUrl);
console.log("serverRootPath:", serverRootPath);
if (!initialProxyBaseUrl) {
proxyBaseUrl = proxyBaseUrl ?? null;
console.log("Updated proxyBaseUrl:", proxyBaseUrl);
return;
}
if (serverRootPath.length > 0 && !initialProxyBaseUrl.endsWith(serverRootPath) && serverRootPath != "/") {
initialProxyBaseUrl += serverRootPath;
proxyBaseUrl = initialProxyBaseUrl;
}
proxyBaseUrl = initialProxyBaseUrl;
console.log("Updated proxyBaseUrl:", proxyBaseUrl);
};
@@ -54,7 +70,11 @@ const updateServerRootPath = (receivedServerRootPath: string) => {
};
export const getProxyBaseUrl = (): string => {
return proxyBaseUrl ? proxyBaseUrl : window.location.origin;
if (proxyBaseUrl) {
return proxyBaseUrl;
}
const browserLocation = getWindowLocation();
return browserLocation?.origin ?? "";
};
const HTTP_REQUEST = {
@@ -159,7 +179,10 @@ const handleError = async (errorData: string) => {
NotificationsManager.info("UI Session Expired. Logging out.");
lastErrorTime = currentTime;
clearTokenCookies();
window.location.href = window.location.pathname;
const browserLocation = getWindowLocation();
if (browserLocation) {
window.location.href = browserLocation.pathname;
}
}
lastErrorTime = currentTime;
} else {
@@ -132,6 +132,13 @@ export const fetchUserModels = async (
}
};
/**
*
* @deprecated
* This component is being DEPRECATED in favor of src/app/(dashboard)/virtual-keys/components/CreateKey.tsx
* Please contribute to the new refactor.
*
*/
const CreateKey: React.FC<CreateKeyProps> = ({
userID,
team,
@@ -47,6 +47,13 @@ interface KeyInfoViewProps {
backButtonText?: string;
}
/**
*
* @deprecated
* This component is being DEPRECATED in favor of src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx
* Please contribute to the new refactor.
*
*/
export default function KeyInfoView({
keyId,
onClose,
@@ -90,7 +90,7 @@ interface ViewKeyTableProps {
selectedTeam: Team | null;
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>;
data: KeyResponse[] | null;
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
setData: (keys: KeyResponse[]) => void;
teams: Team[] | null;
premiumUser: boolean;
currentOrg: Organization | null;
@@ -20,11 +20,12 @@ import ViewUserTeam from "./view_user_team";
import DashboardTeam from "./dashboard_default_team";
import Onboarding from "../app/onboarding/page";
import { useSearchParams, useRouter } from "next/navigation";
import { Team } from "./key_team_helpers/key_list";
import { KeyResponse, Team } from "./key_team_helpers/key_list";
import { jwtDecode } from "jwt-decode";
import { Typography } from "antd";
import { clearTokenCookies } from "@/utils/cookieUtils";
import { clearMCPAuthTokens } from "./mcp_tools/mcp_auth_storage";
import { Setter } from "@/types";
export interface ProxySettings {
PROXY_BASE_URL: string | null;
@@ -56,7 +57,7 @@ interface UserDashboardProps {
setUserRole: React.Dispatch<React.SetStateAction<string>>;
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
setKeys: React.Dispatch<React.SetStateAction<Object[] | null>>;
setKeys: (keys: KeyResponse[]) => void;
premiumUser: boolean;
organizations: Organization[] | null;
addKey: (data: any) => void;
@@ -30,6 +30,8 @@ import { updateExistingKeys } from "@/utils/dataUtils";
import { useDebouncedState } from "@tanstack/react-pacer/debouncer";
import { isAdminRole } from "@/utils/roles";
import NotificationsManager from "./molecules/notifications_manager";
import { Setter } from "@/types";
import { KeyResponse } from "@/components/key_team_helpers/key_list";
interface ViewUserDashboardProps {
accessToken: string | null;
@@ -0,0 +1,112 @@
"use client";
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
};
import React, { createContext, useContext, useEffect, useState } from "react";
import { useRouter } from "next/navigation"; // ⟵ add this
type Flags = {
refactoredUIFlag: boolean;
setRefactoredUIFlag: (v: boolean) => void;
};
const STORAGE_KEY = "feature.refactoredUIFlag";
const FeatureFlagsCtx = createContext<Flags | null>(null);
/** 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 }) => {
const router = useRouter(); // ⟵ add this
// Lazy init reads from localStorage only on the client
const [refactoredUIFlag, setRefactoredUIFlagState] = useState<boolean>(() => 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);
}, []);
// Redirect to base path the moment the flag is OFF.
useEffect(() => {
if (refactoredUIFlag) return; // only act when turned off
const base = getBasePath();
const normalize = (p: string) => (p.endsWith("/") ? p : p + "/");
const current = normalize(window.location.pathname);
// Avoid a redirect loop if we're already at the base path.
if (current !== base) {
// Replace so the "off" redirect doesn't pollute history.
router.replace(base);
}
}, [refactoredUIFlag, router]);
return (
<FeatureFlagsCtx.Provider value={{ refactoredUIFlag, setRefactoredUIFlag }}>{children}</FeatureFlagsCtx.Provider>
);
};
const useFeatureFlags = () => {
const ctx = useContext(FeatureFlagsCtx);
if (!ctx) throw new Error("useFeatureFlags must be used within FeatureFlagsProvider");
return ctx;
};
export default useFeatureFlags;
@@ -6,6 +6,10 @@
* Clears the token cookie from both root and /ui paths
*/
export function clearTokenCookies() {
if (typeof window === "undefined" || typeof document === "undefined") {
return;
}
// Get the current domain
const domain = window.location.hostname;
@@ -37,6 +41,7 @@ export function clearTokenCookies() {
* @returns The cookie value or null if not found
*/
export function getCookie(name: string) {
if (typeof document === "undefined") return null;
const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "="));
return cookieValue ? cookieValue.split("=")[1] : null;
}
+1 -1
View File
@@ -5,7 +5,7 @@ export const all_admin_roles = [...old_admin_roles, ...v2_admin_role_names];
export const internalUserRoles = ["Internal User", "Internal Viewer"];
export const rolesAllowedToSeeUsage = ["Admin", "Admin Viewer", "Internal User", "Internal Viewer"];
export const rolesWithWriteAccess = ["Internal User", "Admin"];
export const rolesWithWriteAccess = ["Internal User", "Admin", "proxy_admin"];
// Helper function to check if a role is in all_admin_roles
export const isAdminRole = (role: string): boolean => {