mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-14 11:08:37 +00:00
Merge pull request #15236 from BerriAI/litellm_ui_refactor_genesis
LiteLLM UI Refactor Infrastructure
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
NODE_ENV=development
|
||||
NEXT_PUBLIC_BASE_URL=""
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" ? (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user