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:
cursor 2026-04-24 06:05:03 +00:00
parent a9985fc17c
commit 63b3d12c73
No known key found for this signature in database
2 changed files with 284 additions and 436 deletions

View file

@ -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