mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
feat(ui): migrate leftnav to shadcn (drops antd Layout/Menu/ConfigProvider)
Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
a9985fc17c
commit
63b3d12c73
2 changed files with 284 additions and 436 deletions
|
|
@ -1,28 +1,63 @@
|
|||
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
|
||||
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { Plug as ApiOutlined, LayoutGrid as AppstoreOutlined, ClipboardCheck as AuditOutlined, Landmark as BankOutlined, BarChart3 as BarChartOutlined, Palette as BgColorsOutlined, Square as BlockOutlined, Book as BookOutlined, CreditCard as CreditCardOutlined, Database as DatabaseOutlined, FlaskConical as ExperimentOutlined, Upload as ExportOutlined, FileText as FileTextOutlined, Folder as FolderOutlined, Key as KeyOutlined, LineChart as LineChartOutlined, PlayCircle as PlayCircleOutlined, Bot as RobotOutlined, ShieldCheck as SafetyOutlined, Search as SearchOutlined, Settings as SettingOutlined, Tags as TagsOutlined, Users as TeamOutlined, Wrench as ToolOutlined, User as UserOutlined } from "lucide-react";
|
||||
import type { MenuProps } from "antd";
|
||||
import { ConfigProvider, Layout, Menu } from "antd";
|
||||
import { useMemo } from "react";
|
||||
import { all_admin_roles, internalUserRoles, isAdminRole, isUserTeamAdminForAnyTeam, rolesWithWriteAccess } from "../utils/roles";
|
||||
import {
|
||||
Plug as ApiOutlined,
|
||||
LayoutGrid as AppstoreOutlined,
|
||||
ClipboardCheck as AuditOutlined,
|
||||
Landmark as BankOutlined,
|
||||
BarChart3 as BarChartOutlined,
|
||||
Palette as BgColorsOutlined,
|
||||
Square as BlockOutlined,
|
||||
Book as BookOutlined,
|
||||
ChevronDown,
|
||||
CreditCard as CreditCardOutlined,
|
||||
Database as DatabaseOutlined,
|
||||
FlaskConical as ExperimentOutlined,
|
||||
ExternalLink as ExportOutlined,
|
||||
FileText as FileTextOutlined,
|
||||
Folder as FolderOutlined,
|
||||
Key as KeyOutlined,
|
||||
LineChart as LineChartOutlined,
|
||||
PlayCircle as PlayCircleOutlined,
|
||||
Bot as RobotOutlined,
|
||||
ShieldCheck as SafetyOutlined,
|
||||
Search as SearchOutlined,
|
||||
Settings as SettingOutlined,
|
||||
Tags as TagsOutlined,
|
||||
Users as TeamOutlined,
|
||||
Wrench as ToolOutlined,
|
||||
User as UserOutlined,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
all_admin_roles,
|
||||
internalUserRoles,
|
||||
isAdminRole,
|
||||
isUserTeamAdminForAnyTeam,
|
||||
rolesWithWriteAccess,
|
||||
} from "../utils/roles";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import NewBadge from "./common_components/NewBadge";
|
||||
import type { Organization } from "./networking";
|
||||
import UsageIndicator from "./UsageIndicator";
|
||||
import { serverRootPath } from "./networking";
|
||||
const { Sider } = Layout;
|
||||
|
||||
/**
|
||||
* Pages migrated to path-based routing under (dashboard)/.
|
||||
* Key = legacy page id, Value = route segment.
|
||||
* Keep in sync with MIGRATED_PAGES in (dashboard)/layout.tsx and
|
||||
* LEGACY_REDIRECTS in app/page.tsx.
|
||||
*/
|
||||
const MIGRATED_PAGES: Record<string, string> = {
|
||||
"api-reference": "api-reference",
|
||||
};
|
||||
|
||||
/** Build an absolute href for a migrated page, respecting base URL + serverRootPath. */
|
||||
/** Build an absolute href for a migrated page. */
|
||||
function migratedHref(routeSegment: string): string {
|
||||
const raw = process.env.NEXT_PUBLIC_BASE_URL ?? "";
|
||||
const trimmed = raw.replace(/^\/+|\/+$/g, "");
|
||||
|
|
@ -37,7 +72,6 @@ function migratedHref(routeSegment: string): string {
|
|||
return `${base}${routeSegment}`;
|
||||
}
|
||||
|
||||
// Define the props type
|
||||
interface SidebarProps {
|
||||
setPage: (page: string) => void;
|
||||
defaultSelectedKey: string;
|
||||
|
|
@ -50,7 +84,6 @@ interface SidebarProps {
|
|||
allowVectorStoresForTeamAdmins?: boolean;
|
||||
}
|
||||
|
||||
// Menu item configuration
|
||||
interface MenuItem {
|
||||
key: string;
|
||||
page: string;
|
||||
|
|
@ -61,72 +94,27 @@ interface MenuItem {
|
|||
external_url?: string;
|
||||
}
|
||||
|
||||
// Group configuration
|
||||
interface MenuGroup {
|
||||
groupLabel: string;
|
||||
items: MenuItem[];
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
// Menu groups organized by category - defined outside component for export
|
||||
const menuGroups: MenuGroup[] = [
|
||||
{
|
||||
groupLabel: "AI GATEWAY",
|
||||
items: [
|
||||
{
|
||||
key: "api-keys",
|
||||
page: "api-keys",
|
||||
label: "Virtual Keys",
|
||||
icon: <KeyOutlined />,
|
||||
},
|
||||
{
|
||||
key: "llm-playground",
|
||||
page: "llm-playground",
|
||||
label: "Playground",
|
||||
icon: <PlayCircleOutlined />,
|
||||
roles: rolesWithWriteAccess,
|
||||
},
|
||||
{
|
||||
key: "models",
|
||||
page: "models",
|
||||
label: "Models + Endpoints",
|
||||
icon: <BlockOutlined />,
|
||||
roles: rolesWithWriteAccess,
|
||||
},
|
||||
{
|
||||
key: "agents",
|
||||
page: "agents",
|
||||
label: "Agents",
|
||||
icon: <RobotOutlined />,
|
||||
roles: rolesWithWriteAccess,
|
||||
},
|
||||
{
|
||||
key: "mcp-servers",
|
||||
page: "mcp-servers",
|
||||
label: "MCP Servers",
|
||||
icon: <ToolOutlined />,
|
||||
},
|
||||
{
|
||||
key: "skills",
|
||||
page: "skills",
|
||||
label: "Skills",
|
||||
icon: <ApiOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "guardrails",
|
||||
page: "guardrails",
|
||||
label: "Guardrails",
|
||||
icon: <SafetyOutlined />,
|
||||
},
|
||||
{ key: "api-keys", page: "api-keys", label: "Virtual Keys", icon: <KeyOutlined /> },
|
||||
{ key: "llm-playground", page: "llm-playground", label: "Playground", icon: <PlayCircleOutlined />, roles: rolesWithWriteAccess },
|
||||
{ key: "models", page: "models", label: "Models + Endpoints", icon: <BlockOutlined />, roles: rolesWithWriteAccess },
|
||||
{ key: "agents", page: "agents", label: "Agents", icon: <RobotOutlined />, roles: rolesWithWriteAccess },
|
||||
{ key: "mcp-servers", page: "mcp-servers", label: "MCP Servers", icon: <ToolOutlined /> },
|
||||
{ key: "skills", page: "skills", label: "Skills", icon: <ApiOutlined />, roles: all_admin_roles },
|
||||
{ key: "guardrails", page: "guardrails", label: "Guardrails", icon: <SafetyOutlined /> },
|
||||
{
|
||||
key: "policies",
|
||||
page: "policies",
|
||||
label: (
|
||||
<span className="flex items-center gap-4">
|
||||
Policies
|
||||
</span>
|
||||
),
|
||||
label: <span className="flex items-center gap-4">Policies</span>,
|
||||
icon: <AuditOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
|
|
@ -136,24 +124,9 @@ const menuGroups: MenuGroup[] = [
|
|||
label: "Tools",
|
||||
icon: <ToolOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "search-tools",
|
||||
page: "search-tools",
|
||||
label: "Search Tools",
|
||||
icon: <SearchOutlined />,
|
||||
},
|
||||
{
|
||||
key: "vector-stores",
|
||||
page: "vector-stores",
|
||||
label: "Vector Stores",
|
||||
icon: <DatabaseOutlined />,
|
||||
},
|
||||
{
|
||||
key: "tool-policies",
|
||||
page: "tool-policies",
|
||||
label: "Tool Policies",
|
||||
icon: <SafetyOutlined />,
|
||||
},
|
||||
{ key: "search-tools", page: "search-tools", label: "Search Tools", icon: <SearchOutlined /> },
|
||||
{ key: "vector-stores", page: "vector-stores", label: "Vector Stores", icon: <DatabaseOutlined /> },
|
||||
{ key: "tool-policies", page: "tool-policies", label: "Tool Policies", icon: <SafetyOutlined /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
@ -161,141 +134,45 @@ const menuGroups: MenuGroup[] = [
|
|||
{
|
||||
groupLabel: "OBSERVABILITY",
|
||||
items: [
|
||||
{
|
||||
key: "new_usage",
|
||||
page: "new_usage",
|
||||
icon: <BarChartOutlined />,
|
||||
roles: [...all_admin_roles, ...internalUserRoles],
|
||||
label: "Usage",
|
||||
},
|
||||
{
|
||||
key: "logs",
|
||||
page: "logs",
|
||||
label: "Logs",
|
||||
icon: <LineChartOutlined />,
|
||||
},
|
||||
{
|
||||
key: "guardrails-monitor",
|
||||
page: "guardrails-monitor",
|
||||
label: "Guardrails Monitor",
|
||||
icon: <SafetyOutlined />,
|
||||
roles: [...all_admin_roles, ...internalUserRoles],
|
||||
},
|
||||
{ key: "new_usage", page: "new_usage", icon: <BarChartOutlined />, roles: [...all_admin_roles, ...internalUserRoles], label: "Usage" },
|
||||
{ key: "logs", page: "logs", label: "Logs", icon: <LineChartOutlined /> },
|
||||
{ key: "guardrails-monitor", page: "guardrails-monitor", label: "Guardrails Monitor", icon: <SafetyOutlined />, roles: [...all_admin_roles, ...internalUserRoles] },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "ACCESS CONTROL",
|
||||
items: [
|
||||
{
|
||||
key: "teams",
|
||||
page: "teams",
|
||||
label: "Teams",
|
||||
icon: <TeamOutlined />,
|
||||
},
|
||||
{ key: "teams", page: "teams", label: "Teams", icon: <TeamOutlined /> },
|
||||
{
|
||||
key: "projects",
|
||||
page: "projects",
|
||||
label: (
|
||||
<span className="flex items-center gap-2">
|
||||
Projects <NewBadge />
|
||||
</span>
|
||||
),
|
||||
label: <span className="flex items-center gap-2">Projects <NewBadge /></span>,
|
||||
icon: <FolderOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "users",
|
||||
page: "users",
|
||||
label: "Internal Users",
|
||||
icon: <UserOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "organizations",
|
||||
page: "organizations",
|
||||
label: "Organizations",
|
||||
icon: <BankOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "access-groups",
|
||||
page: "access-groups",
|
||||
label: "Access Groups",
|
||||
icon: <BlockOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "budgets",
|
||||
page: "budgets",
|
||||
label: "Budgets",
|
||||
icon: <CreditCardOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{ key: "users", page: "users", label: "Internal Users", icon: <UserOutlined />, roles: all_admin_roles },
|
||||
{ key: "organizations", page: "organizations", label: "Organizations", icon: <BankOutlined />, roles: all_admin_roles },
|
||||
{ key: "access-groups", page: "access-groups", label: "Access Groups", icon: <BlockOutlined />, roles: all_admin_roles },
|
||||
{ key: "budgets", page: "budgets", label: "Budgets", icon: <CreditCardOutlined />, roles: all_admin_roles },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "DEVELOPER TOOLS",
|
||||
items: [
|
||||
{
|
||||
key: "api-reference",
|
||||
page: "api-reference",
|
||||
label: "API Reference",
|
||||
icon: <ApiOutlined />,
|
||||
},
|
||||
{
|
||||
key: "model-hub-table",
|
||||
page: "model-hub-table",
|
||||
label: "AI Hub",
|
||||
icon: <AppstoreOutlined />,
|
||||
},
|
||||
|
||||
{
|
||||
key: "learning-resources",
|
||||
page: "learning-resources",
|
||||
label: "Learning Resources",
|
||||
icon: <BookOutlined />,
|
||||
external_url: "https://models.litellm.ai/cookbook",
|
||||
},
|
||||
{ key: "api-reference", page: "api-reference", label: "API Reference", icon: <ApiOutlined /> },
|
||||
{ key: "model-hub-table", page: "model-hub-table", label: "AI Hub", icon: <AppstoreOutlined /> },
|
||||
{ key: "learning-resources", page: "learning-resources", label: "Learning Resources", icon: <BookOutlined />, external_url: "https://models.litellm.ai/cookbook" },
|
||||
{
|
||||
key: "experimental",
|
||||
page: "experimental",
|
||||
label: "Experimental",
|
||||
icon: <ExperimentOutlined />,
|
||||
children: [
|
||||
{
|
||||
key: "caching",
|
||||
page: "caching",
|
||||
label: "Caching",
|
||||
icon: <DatabaseOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "prompts",
|
||||
page: "prompts",
|
||||
label: "Prompts",
|
||||
icon: <FileTextOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "transform-request",
|
||||
page: "transform-request",
|
||||
label: "API Playground",
|
||||
icon: <ApiOutlined />,
|
||||
roles: [...all_admin_roles, ...internalUserRoles],
|
||||
},
|
||||
{
|
||||
key: "tag-management",
|
||||
page: "tag-management",
|
||||
label: "Tag Management",
|
||||
icon: <TagsOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "4",
|
||||
page: "usage",
|
||||
label: "Old Usage",
|
||||
icon: <BarChartOutlined />,
|
||||
}
|
||||
{ key: "caching", page: "caching", label: "Caching", icon: <DatabaseOutlined />, roles: all_admin_roles },
|
||||
{ key: "prompts", page: "prompts", label: "Prompts", icon: <FileTextOutlined />, roles: all_admin_roles },
|
||||
{ key: "transform-request", page: "transform-request", label: "API Playground", icon: <ApiOutlined />, roles: [...all_admin_roles, ...internalUserRoles] },
|
||||
{ key: "tag-management", page: "tag-management", label: "Tag Management", icon: <TagsOutlined />, roles: all_admin_roles },
|
||||
{ key: "4", page: "usage", label: "Old Usage", icon: <BarChartOutlined /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
@ -307,28 +184,12 @@ const menuGroups: MenuGroup[] = [
|
|||
{
|
||||
key: "settings",
|
||||
page: "settings",
|
||||
label: (
|
||||
<span className="flex items-center gap-2">
|
||||
Settings <NewBadge />
|
||||
</span>
|
||||
),
|
||||
label: <span className="flex items-center gap-2">Settings <NewBadge /></span>,
|
||||
icon: <SettingOutlined />,
|
||||
roles: all_admin_roles,
|
||||
children: [
|
||||
{
|
||||
key: "router-settings",
|
||||
page: "router-settings",
|
||||
label: "Router Settings",
|
||||
icon: <SettingOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "logging-and-alerts",
|
||||
page: "logging-and-alerts",
|
||||
label: "Logging & Alerts",
|
||||
icon: <SettingOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{ key: "router-settings", page: "router-settings", label: "Router Settings", icon: <SettingOutlined />, roles: all_admin_roles },
|
||||
{ key: "logging-and-alerts", page: "logging-and-alerts", label: "Logging & Alerts", icon: <SettingOutlined />, roles: all_admin_roles },
|
||||
{
|
||||
key: "admin-panel",
|
||||
page: "admin-panel",
|
||||
|
|
@ -340,45 +201,65 @@ const menuGroups: MenuGroup[] = [
|
|||
icon: <SettingOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "cost-tracking",
|
||||
page: "cost-tracking",
|
||||
label: "Cost Tracking",
|
||||
icon: <BarChartOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "ui-theme",
|
||||
page: "ui-theme",
|
||||
label: "UI Theme",
|
||||
icon: <BgColorsOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{ key: "cost-tracking", page: "cost-tracking", label: "Cost Tracking", icon: <BarChartOutlined />, roles: all_admin_roles },
|
||||
{ key: "ui-theme", page: "ui-theme", label: "UI Theme", icon: <BgColorsOutlined />, roles: all_admin_roles },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapsed = false, enabledPagesInternalUsers, enableProjectsUI, disableAgentsForInternalUsers, allowAgentsForTeamAdmins, disableVectorStoresForInternalUsers, allowVectorStoresForTeamAdmins }) => {
|
||||
const Sidebar: React.FC<SidebarProps> = ({
|
||||
setPage,
|
||||
defaultSelectedKey,
|
||||
collapsed = false,
|
||||
enabledPagesInternalUsers,
|
||||
enableProjectsUI,
|
||||
disableAgentsForInternalUsers,
|
||||
allowAgentsForTeamAdmins,
|
||||
disableVectorStoresForInternalUsers,
|
||||
allowVectorStoresForTeamAdmins,
|
||||
}) => {
|
||||
const { userId, accessToken, userRole } = useAuthorized();
|
||||
const { data: organizations } = useOrganizations();
|
||||
const { data: teams } = useTeams();
|
||||
|
||||
// Check if user is an org_admin
|
||||
const isOrgAdmin = useMemo(() => {
|
||||
if (!userId || !organizations) return false;
|
||||
return organizations.some((org: Organization) =>
|
||||
org.members?.some((member) => member.user_id === userId && member.user_role === "org_admin"),
|
||||
org.members?.some(
|
||||
(member) => member.user_id === userId && member.user_role === "org_admin",
|
||||
),
|
||||
);
|
||||
}, [userId, organizations]);
|
||||
|
||||
// Check if user is a team admin for any team
|
||||
const isTeamAdmin = useMemo(() => isUserTeamAdminForAnyTeam(teams ?? null, userId ?? ""), [teams, userId]);
|
||||
const isTeamAdmin = useMemo(
|
||||
() => isUserTeamAdminForAnyTeam(teams ?? null, userId ?? ""),
|
||||
[teams, userId],
|
||||
);
|
||||
|
||||
// Auto-open any parent submenu that contains the currently selected page.
|
||||
const initialOpenKeys = useMemo(() => {
|
||||
const open: string[] = [];
|
||||
for (const group of menuGroups) {
|
||||
for (const item of group.items) {
|
||||
if (item.children?.some((c) => c.page === defaultSelectedKey)) {
|
||||
open.push(item.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return open;
|
||||
}, [defaultSelectedKey]);
|
||||
|
||||
const [openKeys, setOpenKeys] = useState<string[]>(initialOpenKeys);
|
||||
|
||||
const toggleKey = (key: string) => {
|
||||
setOpenKeys((prev) =>
|
||||
prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key],
|
||||
);
|
||||
};
|
||||
|
||||
// Navigate to page helper
|
||||
const navigateToPage = (page: string) => {
|
||||
// For migrated pages, just call setPage — the parent layout handles routing
|
||||
if (MIGRATED_PAGES[page]) {
|
||||
setPage(page);
|
||||
return;
|
||||
|
|
@ -389,242 +270,209 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
setPage(page);
|
||||
};
|
||||
|
||||
// Wrap label in <a> so every nav item supports right-click → "Open in new tab"
|
||||
// and Ctrl/Cmd+click to open in a new tab, while preserving SPA navigation for normal clicks.
|
||||
const renderNavLink = (
|
||||
label: React.ReactNode,
|
||||
page: string,
|
||||
externalUrl?: string,
|
||||
): React.ReactNode => {
|
||||
if (externalUrl) {
|
||||
return (
|
||||
<a
|
||||
href={externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ color: "inherit", textDecoration: "none" }}
|
||||
>
|
||||
{label} <ExportOutlined style={{ fontSize: 10, marginLeft: 4 }} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
// For migrated pages, generate a path-based href for right-click "Open in new tab"
|
||||
const migratedRoute = MIGRATED_PAGES[page];
|
||||
const href = migratedRoute
|
||||
? migratedHref(migratedRoute)
|
||||
: (() => { const params = new URLSearchParams(window.location.search); params.set("page", page); return `?${params.toString()}`; })();
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
}}
|
||||
style={{ color: "inherit", textDecoration: "none" }}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
// Filter items based on user role and enabled pages for internal users
|
||||
const filterItemsByRole = (items: MenuItem[]): MenuItem[] => {
|
||||
const isAdmin = isAdminRole(userRole);
|
||||
|
||||
// Debug logging
|
||||
if (enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) {
|
||||
console.log("[LeftNav] Filtering with enabled pages:", {
|
||||
userRole,
|
||||
isAdmin,
|
||||
enabledPagesInternalUsers,
|
||||
});
|
||||
}
|
||||
|
||||
return items
|
||||
.map((item) => ({
|
||||
...item,
|
||||
children: item.children ? filterItemsByRole(item.children) : undefined,
|
||||
}))
|
||||
.filter((item) => {
|
||||
// Special handling for organizations and users menu items - allow org_admins
|
||||
if (item.key === "organizations" || item.key === "users") {
|
||||
const hasRoleAccess = !item.roles || item.roles.includes(userRole) || isOrgAdmin;
|
||||
const hasRoleAccess =
|
||||
!item.roles || item.roles.includes(userRole) || isOrgAdmin;
|
||||
if (!hasRoleAccess) return false;
|
||||
|
||||
// Check enabled pages for internal users (non-admins)
|
||||
if (!isAdmin && enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) {
|
||||
const isIncluded = enabledPagesInternalUsers.includes(item.page);
|
||||
console.log(`[LeftNav] Page "${item.page}" (${item.key}): ${isIncluded ? "VISIBLE" : "HIDDEN"}`);
|
||||
return isIncluded;
|
||||
if (
|
||||
!isAdmin &&
|
||||
enabledPagesInternalUsers !== null &&
|
||||
enabledPagesInternalUsers !== undefined
|
||||
) {
|
||||
return enabledPagesInternalUsers.includes(item.page);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hide Projects page if enableProjectsUI is not enabled
|
||||
if (item.key === "projects" && !enableProjectsUI) return false;
|
||||
|
||||
// Hide agents and vector-stores pages for non-admin users when disabled,
|
||||
// unless allow_*_for_team_admins is on and the user is a team admin.
|
||||
if (!isAdmin && item.key === "agents" && disableAgentsForInternalUsers && !(allowAgentsForTeamAdmins && isTeamAdmin)) return false;
|
||||
if (!isAdmin && item.key === "vector-stores" && disableVectorStoresForInternalUsers && !(allowVectorStoresForTeamAdmins && isTeamAdmin)) return false;
|
||||
|
||||
// Existing role check
|
||||
if (
|
||||
!isAdmin &&
|
||||
item.key === "agents" &&
|
||||
disableAgentsForInternalUsers &&
|
||||
!(allowAgentsForTeamAdmins && isTeamAdmin)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!isAdmin &&
|
||||
item.key === "vector-stores" &&
|
||||
disableVectorStoresForInternalUsers &&
|
||||
!(allowVectorStoresForTeamAdmins && isTeamAdmin)
|
||||
)
|
||||
return false;
|
||||
if (item.roles && !item.roles.includes(userRole)) return false;
|
||||
|
||||
// Check enabled pages for internal users (non-admins)
|
||||
if (!isAdmin && enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) {
|
||||
// If item has children, check if any children are visible
|
||||
if (
|
||||
!isAdmin &&
|
||||
enabledPagesInternalUsers !== null &&
|
||||
enabledPagesInternalUsers !== undefined
|
||||
) {
|
||||
if (item.children && item.children.length > 0) {
|
||||
const hasVisibleChildren = item.children.some((child) =>
|
||||
enabledPagesInternalUsers.includes(child.page)
|
||||
enabledPagesInternalUsers.includes(child.page),
|
||||
);
|
||||
if (hasVisibleChildren) {
|
||||
console.log(`[LeftNav] Parent "${item.page}" (${item.key}): VISIBLE (has visible children)`);
|
||||
return true;
|
||||
}
|
||||
if (hasVisibleChildren) return true;
|
||||
}
|
||||
|
||||
const isIncluded = enabledPagesInternalUsers.includes(item.page);
|
||||
console.log(`[LeftNav] Page "${item.page}" (${item.key}): ${isIncluded ? "VISIBLE" : "HIDDEN"}`);
|
||||
return isIncluded;
|
||||
return enabledPagesInternalUsers.includes(item.page);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// Build menu items with groups
|
||||
const buildMenuItems = (): MenuProps["items"] => {
|
||||
const items: MenuProps["items"] = [];
|
||||
// Build href: either migrated-route or ?page=... query.
|
||||
const hrefFor = (item: MenuItem): string => {
|
||||
if (item.external_url) return item.external_url;
|
||||
const migratedRoute = MIGRATED_PAGES[item.page];
|
||||
if (migratedRoute) return migratedHref(migratedRoute);
|
||||
if (typeof window === "undefined") return `?page=${item.page}`;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set("page", item.page);
|
||||
return `?${params.toString()}`;
|
||||
};
|
||||
|
||||
menuGroups.forEach((group) => {
|
||||
// Check if group has role restriction
|
||||
if (group.roles && !group.roles.includes(userRole)) {
|
||||
const renderItem = (item: MenuItem, depth: number) => {
|
||||
const isSelected = item.page === defaultSelectedKey;
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isOpen = openKeys.includes(item.key);
|
||||
const indent = depth * 12;
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) return;
|
||||
e.preventDefault();
|
||||
if (item.external_url) {
|
||||
window.open(item.external_url, "_blank");
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredItems = filterItemsByRole(group.items);
|
||||
if (filteredItems.length === 0) return;
|
||||
|
||||
// Add group with items
|
||||
items.push({
|
||||
type: "group",
|
||||
label: collapsed ? null : (
|
||||
<span
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
fontWeight: 600,
|
||||
color: "#6b7280",
|
||||
letterSpacing: "0.05em",
|
||||
padding: "12px 0 4px 12px",
|
||||
display: "block",
|
||||
marginBottom: "2px",
|
||||
}}
|
||||
>
|
||||
{group.groupLabel}
|
||||
</span>
|
||||
),
|
||||
children: filteredItems.map((item) => ({
|
||||
key: item.key,
|
||||
icon: item.icon,
|
||||
label: renderNavLink(item.label, item.page, item.external_url),
|
||||
children: item.children?.map((child) => ({
|
||||
key: child.key,
|
||||
icon: child.icon,
|
||||
label: renderNavLink(child.label, child.page, child.external_url),
|
||||
onClick: () => {
|
||||
if (child.external_url) {
|
||||
window.open(child.external_url, "_blank");
|
||||
} else {
|
||||
navigateToPage(child.page);
|
||||
}
|
||||
},
|
||||
})),
|
||||
onClick: !item.children
|
||||
? () => {
|
||||
if (item.external_url) {
|
||||
window.open(item.external_url, "_blank");
|
||||
} else {
|
||||
navigateToPage(item.page);
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
// Find selected menu key
|
||||
const findMenuItemKey = (page: string): string => {
|
||||
for (const group of menuGroups) {
|
||||
for (const item of group.items) {
|
||||
if (item.page === page) return item.key;
|
||||
if (item.children) {
|
||||
const child = item.children.find((c) => c.page === page);
|
||||
if (child) return child.key;
|
||||
}
|
||||
if (hasChildren) {
|
||||
toggleKey(item.key);
|
||||
} else {
|
||||
navigateToPage(item.page);
|
||||
}
|
||||
}
|
||||
return "api-keys";
|
||||
};
|
||||
|
||||
const linkContent = (
|
||||
<>
|
||||
{item.icon && (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center shrink-0",
|
||||
"h-[15px] w-[15px]",
|
||||
"[&>svg]:h-[15px] [&>svg]:w-[15px]",
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
{!collapsed && (
|
||||
<span className="flex-1 truncate">
|
||||
{item.label}
|
||||
{item.external_url && (
|
||||
<ExportOutlined className="h-2.5 w-2.5 ml-1 inline" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{!collapsed && hasChildren && (
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 transition-transform text-muted-foreground",
|
||||
isOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const itemClasses = cn(
|
||||
"flex items-center gap-2 rounded-md text-[13px] px-2 mx-1 h-[30px]",
|
||||
"transition-colors cursor-pointer",
|
||||
"text-foreground no-underline",
|
||||
isSelected
|
||||
? "bg-primary/10 text-primary font-medium"
|
||||
: "hover:bg-muted",
|
||||
);
|
||||
|
||||
const link = (
|
||||
<a
|
||||
href={hrefFor(item)}
|
||||
onClick={handleClick}
|
||||
style={{ paddingLeft: `${8 + indent}px` }}
|
||||
className={itemClasses}
|
||||
>
|
||||
{linkContent}
|
||||
</a>
|
||||
);
|
||||
|
||||
// When collapsed, wrap icon with a tooltip that shows the label.
|
||||
const wrapped = collapsed ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{link}</TooltipTrigger>
|
||||
<TooltipContent side="right">{item.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
link
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={item.key} role="none">
|
||||
{wrapped}
|
||||
{hasChildren && !collapsed && isOpen && (
|
||||
<ul role="group" className="mt-[2px] mb-[2px]">
|
||||
{item.children!.map((child) => renderItem(child, depth + 1))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const selectedMenuKey = findMenuItemKey(defaultSelectedKey);
|
||||
const filteredGroups = menuGroups
|
||||
.filter((group) => !group.roles || group.roles.includes(userRole))
|
||||
.map((group) => ({
|
||||
...group,
|
||||
items: filterItemsByRole(group.items),
|
||||
}))
|
||||
.filter((group) => group.items.length > 0);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<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",
|
||||
}}
|
||||
<aside
|
||||
className={cn(
|
||||
"relative bg-background",
|
||||
"transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]",
|
||||
)}
|
||||
style={{ width: collapsed ? 80 : 220 }}
|
||||
>
|
||||
<nav
|
||||
aria-label="Main navigation"
|
||||
className="custom-sidebar-menu pt-1"
|
||||
>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Menu: {
|
||||
iconSize: 15,
|
||||
fontSize: 13,
|
||||
itemMarginInline: 4,
|
||||
itemPaddingInline: 8,
|
||||
itemHeight: 30,
|
||||
itemBorderRadius: 6,
|
||||
subMenuItemBorderRadius: 6,
|
||||
groupTitleFontSize: 10,
|
||||
groupTitleLineHeight: 1.5,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[selectedMenuKey]}
|
||||
defaultOpenKeys={[]}
|
||||
inlineCollapsed={collapsed}
|
||||
className="custom-sidebar-menu"
|
||||
style={{
|
||||
borderRight: 0,
|
||||
backgroundColor: "transparent",
|
||||
fontSize: "13px",
|
||||
paddingTop: "4px",
|
||||
}}
|
||||
items={buildMenuItems()}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
{isAdminRole(userRole) && !collapsed && <UsageIndicator accessToken={accessToken} width={220} />}
|
||||
</Sider>
|
||||
</Layout>
|
||||
<ul role="menu" className="list-none p-0 m-0">
|
||||
{filteredGroups.map((group) => (
|
||||
<li key={group.groupLabel} role="none">
|
||||
{!collapsed && (
|
||||
<div className="text-[10px] font-semibold text-muted-foreground tracking-wider pt-3 pb-1 pl-3 mb-[2px]">
|
||||
{group.groupLabel}
|
||||
</div>
|
||||
)}
|
||||
<ul role="group" className="list-none p-0 m-0">
|
||||
{group.items.map((item) => renderItem(item, 0))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
{isAdminRole(userRole) && !collapsed && (
|
||||
<UsageIndicator accessToken={accessToken} width={220} />
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue