mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-14 22:21:29 +00:00
[Feat] UI - show UI version on top left near logo (#17891)
* left nav * fix order * fix * add ui version on ui * fix link
This commit is contained in:
@@ -21,7 +21,8 @@ import {
|
||||
ToolOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Badge, ConfigProvider, Layout, Menu } from "antd";
|
||||
import { ConfigProvider, Layout, Menu } from "antd";
|
||||
import type { MenuProps } from "antd";
|
||||
import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "../utils/roles";
|
||||
import UsageIndicator from "./usage_indicator";
|
||||
const { Sider } = Layout;
|
||||
@@ -35,237 +36,336 @@ interface SidebarProps {
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
// Create a more comprehensive menu item configuration
|
||||
// Menu item configuration
|
||||
interface MenuItem {
|
||||
key: string;
|
||||
page: string;
|
||||
label: string | React.ReactNode;
|
||||
roles?: string[];
|
||||
children?: MenuItem[]; // Add children property for submenus
|
||||
children?: MenuItem[];
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
// Group configuration
|
||||
interface MenuGroup {
|
||||
groupLabel: string;
|
||||
items: MenuItem[];
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defaultSelectedKey, collapsed = false }) => {
|
||||
// Note: If a menu item does not have a role, it is visible to all roles.
|
||||
const menuItems: MenuItem[] = [
|
||||
// Navigate to page helper
|
||||
const navigateToPage = (page: string) => {
|
||||
const newSearchParams = new URLSearchParams(window.location.search);
|
||||
newSearchParams.set("page", page);
|
||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
|
||||
setPage(page);
|
||||
};
|
||||
|
||||
// Menu groups organized by category
|
||||
const menuGroups: MenuGroup[] = [
|
||||
{
|
||||
key: "api-keys",
|
||||
page: "api-keys",
|
||||
label: "Virtual Keys",
|
||||
icon: <KeyOutlined style={{ fontSize: "18px" }} />,
|
||||
},
|
||||
{
|
||||
key: "llm-playground",
|
||||
page: "llm-playground",
|
||||
label: "Playground",
|
||||
icon: <PlayCircleOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: rolesWithWriteAccess,
|
||||
},
|
||||
{
|
||||
key: "models",
|
||||
page: "models",
|
||||
label: "Models + Endpoints",
|
||||
icon: <BlockOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: rolesWithWriteAccess,
|
||||
},
|
||||
{
|
||||
key: "new_usage",
|
||||
page: "new_usage",
|
||||
label: (
|
||||
<span className="flex items-center gap-4">
|
||||
Usage <Badge color="blue" count="New" />
|
||||
</span>
|
||||
),
|
||||
icon: <BarChartOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: [...all_admin_roles, ...internalUserRoles],
|
||||
},
|
||||
{ key: "teams", page: "teams", label: "Teams", icon: <TeamOutlined style={{ fontSize: "18px" }} /> },
|
||||
{
|
||||
key: "organizations",
|
||||
page: "organizations",
|
||||
label: "Organizations",
|
||||
icon: <BankOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "users",
|
||||
page: "users",
|
||||
label: "Internal Users",
|
||||
icon: <UserOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "budgets",
|
||||
page: "budgets",
|
||||
label: "Budgets",
|
||||
icon: <CreditCardOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{ key: "api_ref", page: "api_ref", label: "API Reference", icon: <ApiOutlined style={{ fontSize: "18px" }} /> },
|
||||
{
|
||||
key: "model-hub-table",
|
||||
page: "model-hub-table",
|
||||
label: "AI Hub",
|
||||
icon: <AppstoreOutlined style={{ fontSize: "18px" }} />,
|
||||
},
|
||||
{ key: "logs", page: "logs", label: "Logs", icon: <LineChartOutlined style={{ fontSize: "18px" }} /> },
|
||||
{
|
||||
key: "guardrails",
|
||||
page: "guardrails",
|
||||
label: "Guardrails",
|
||||
icon: <SafetyOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "mcp-servers",
|
||||
page: "mcp-servers",
|
||||
label: "MCP Servers",
|
||||
icon: <ToolOutlined style={{ fontSize: "18px" }} />,
|
||||
},
|
||||
{
|
||||
key: "tools",
|
||||
page: "tools",
|
||||
label: "Tools",
|
||||
icon: <ToolOutlined style={{ fontSize: "18px" }} />,
|
||||
children: [
|
||||
groupLabel: "AI GATEWAY",
|
||||
items: [
|
||||
{
|
||||
key: "search-tools",
|
||||
page: "search-tools",
|
||||
label: "Search Tools",
|
||||
icon: <SearchOutlined style={{ fontSize: "18px" }} />,
|
||||
key: "api-keys",
|
||||
page: "api-keys",
|
||||
label: "Virtual Keys",
|
||||
icon: <KeyOutlined />,
|
||||
},
|
||||
{
|
||||
key: "vector-stores",
|
||||
page: "vector-stores",
|
||||
label: "Vector Stores",
|
||||
icon: <DatabaseOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
key: "llm-playground",
|
||||
page: "llm-playground",
|
||||
label: "Playground",
|
||||
icon: <PlayCircleOutlined />,
|
||||
roles: rolesWithWriteAccess,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "experimental",
|
||||
page: "experimental",
|
||||
label: "Experimental",
|
||||
icon: <ExperimentOutlined style={{ fontSize: "18px" }} />,
|
||||
children: [
|
||||
{
|
||||
key: "caching",
|
||||
page: "caching",
|
||||
label: "Caching",
|
||||
icon: <DatabaseOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
key: "models",
|
||||
page: "models",
|
||||
label: "Models + Endpoints",
|
||||
icon: <BlockOutlined />,
|
||||
roles: rolesWithWriteAccess,
|
||||
},
|
||||
{
|
||||
key: "agents",
|
||||
page: "agents",
|
||||
label: "Agents",
|
||||
icon: <RobotOutlined style={{ fontSize: "18px" }} />,
|
||||
icon: <RobotOutlined />,
|
||||
roles: rolesWithWriteAccess,
|
||||
},
|
||||
{
|
||||
key: "prompts",
|
||||
page: "prompts",
|
||||
label: "Prompts",
|
||||
icon: <FileTextOutlined style={{ fontSize: "18px" }} />,
|
||||
key: "mcp-servers",
|
||||
page: "mcp-servers",
|
||||
label: "MCP Servers",
|
||||
icon: <ToolOutlined />,
|
||||
},
|
||||
{
|
||||
key: "guardrails",
|
||||
page: "guardrails",
|
||||
label: "Guardrails",
|
||||
icon: <SafetyOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "transform-request",
|
||||
page: "transform-request",
|
||||
label: "API Playground",
|
||||
icon: <ApiOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: [...all_admin_roles, ...internalUserRoles],
|
||||
key: "tools",
|
||||
page: "tools",
|
||||
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 />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "tag-management",
|
||||
page: "tag-management",
|
||||
label: "Tag Management",
|
||||
icon: <TagsOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{ key: "4", page: "usage", label: "Old Usage", icon: <BarChartOutlined style={{ fontSize: "18px" }} /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
page: "settings",
|
||||
label: "Settings",
|
||||
icon: <SettingOutlined style={{ fontSize: "18px" }} />,
|
||||
groupLabel: "OBSERVABILITY",
|
||||
items: [
|
||||
{
|
||||
key: "new_usage",
|
||||
page: "new_usage",
|
||||
label: "Usage",
|
||||
icon: <BarChartOutlined />,
|
||||
roles: [...all_admin_roles, ...internalUserRoles],
|
||||
},
|
||||
{
|
||||
key: "logs",
|
||||
page: "logs",
|
||||
label: "Logs",
|
||||
icon: <LineChartOutlined />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "ACCESS CONTROL",
|
||||
items: [
|
||||
{
|
||||
key: "users",
|
||||
page: "users",
|
||||
label: "Internal Users",
|
||||
icon: <UserOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "teams",
|
||||
page: "teams",
|
||||
label: "Teams",
|
||||
icon: <TeamOutlined />,
|
||||
},
|
||||
{
|
||||
key: "organizations",
|
||||
page: "organizations",
|
||||
label: "Organizations",
|
||||
icon: <BankOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "budgets",
|
||||
page: "budgets",
|
||||
label: "Budgets",
|
||||
icon: <CreditCardOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "DEVELOPER TOOLS",
|
||||
items: [
|
||||
{
|
||||
key: "api_ref",
|
||||
page: "api_ref",
|
||||
label: "API Reference",
|
||||
icon: <ApiOutlined />,
|
||||
},
|
||||
{
|
||||
key: "model-hub-table",
|
||||
page: "model-hub-table",
|
||||
label: "AI Hub",
|
||||
icon: <AppstoreOutlined />,
|
||||
},
|
||||
{
|
||||
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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "SETTINGS",
|
||||
roles: all_admin_roles,
|
||||
children: [
|
||||
items: [
|
||||
{
|
||||
key: "router-settings",
|
||||
page: "router-settings",
|
||||
label: "Router Settings",
|
||||
icon: <SettingOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "logging-and-alerts",
|
||||
page: "logging-and-alerts",
|
||||
label: "Logging & Alerts",
|
||||
icon: <SettingOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "admin-panel",
|
||||
page: "admin-panel",
|
||||
label: "Admin Settings",
|
||||
icon: <SettingOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "cost-tracking",
|
||||
page: "cost-tracking",
|
||||
label: "Cost Tracking",
|
||||
icon: <BarChartOutlined style={{ fontSize: "18px" }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "ui-theme",
|
||||
page: "ui-theme",
|
||||
label: "UI Theme",
|
||||
icon: <BgColorsOutlined style={{ fontSize: "18px" }} />,
|
||||
key: "settings",
|
||||
page: "settings",
|
||||
label: "Settings",
|
||||
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: "admin-panel",
|
||||
page: "admin-panel",
|
||||
label: "Admin Settings",
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
// Find the menu item that matches the default page, including in submenus
|
||||
const findMenuItemKey = (page: string): string => {
|
||||
// Check top-level items
|
||||
const topLevelItem = menuItems.find((item) => item.page === page);
|
||||
if (topLevelItem) return topLevelItem.key;
|
||||
|
||||
// Check submenu items
|
||||
for (const item of menuItems) {
|
||||
if (item.children) {
|
||||
const childItem = item.children.find((child) => child.page === page);
|
||||
if (childItem) return childItem.key;
|
||||
// Filter items based on user role
|
||||
const filterItemsByRole = (items: MenuItem[]): MenuItem[] => {
|
||||
return items
|
||||
.filter((item) => !item.roles || item.roles.includes(userRole))
|
||||
.map((item) => ({
|
||||
...item,
|
||||
children: item.children ? filterItemsByRole(item.children) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
// Build menu items with groups
|
||||
const buildMenuItems = (): MenuProps["items"] => {
|
||||
const items: MenuProps["items"] = [];
|
||||
|
||||
menuGroups.forEach((group) => {
|
||||
// Check if group has role restriction
|
||||
if (group.roles && !group.roles.includes(userRole)) {
|
||||
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: item.label,
|
||||
children: item.children?.map((child) => ({
|
||||
key: child.key,
|
||||
icon: child.icon,
|
||||
label: child.label,
|
||||
onClick: () => navigateToPage(child.page),
|
||||
})),
|
||||
onClick: !item.children ? () => 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "1"; // Default to first item if not found
|
||||
return "api-keys";
|
||||
};
|
||||
|
||||
const selectedMenuKey = findMenuItemKey(defaultSelectedKey);
|
||||
|
||||
const filteredMenuItems = menuItems.filter((item) => {
|
||||
// Check if parent item has roles and user has access
|
||||
const hasParentAccess = !item.roles || item.roles.includes(userRole);
|
||||
|
||||
console.log(`Menu item ${item.label}: roles=${item.roles}, userRole=${userRole}, hasAccess=${hasParentAccess}`);
|
||||
|
||||
if (!hasParentAccess) return false;
|
||||
|
||||
// Filter children if they exist
|
||||
if (item.children) {
|
||||
item.children = item.children.filter((child) => !child.roles || child.roles.includes(userRole));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Sider
|
||||
@@ -276,7 +376,7 @@ const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defau
|
||||
collapsible
|
||||
trigger={null}
|
||||
style={{
|
||||
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", // Material Design easing
|
||||
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
@@ -284,8 +384,15 @@ const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defau
|
||||
theme={{
|
||||
components: {
|
||||
Menu: {
|
||||
iconSize: 18,
|
||||
fontSize: 14,
|
||||
iconSize: 15,
|
||||
fontSize: 13,
|
||||
itemMarginInline: 4,
|
||||
itemPaddingInline: 8,
|
||||
itemHeight: 30,
|
||||
itemBorderRadius: 6,
|
||||
subMenuItemBorderRadius: 6,
|
||||
groupTitleFontSize: 10,
|
||||
groupTitleLineHeight: 1.5,
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -293,38 +400,16 @@ const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defau
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[selectedMenuKey]}
|
||||
defaultOpenKeys={collapsed ? [] : ["llm-tools"]}
|
||||
defaultOpenKeys={[]}
|
||||
inlineCollapsed={collapsed}
|
||||
className="custom-sidebar-menu"
|
||||
style={{
|
||||
borderRight: 0,
|
||||
backgroundColor: "transparent",
|
||||
fontSize: "14px",
|
||||
fontSize: "13px",
|
||||
paddingTop: "4px",
|
||||
}}
|
||||
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: () => {
|
||||
const newSearchParams = new URLSearchParams(window.location.search);
|
||||
newSearchParams.set("page", child.page);
|
||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
|
||||
setPage(child.page);
|
||||
},
|
||||
})),
|
||||
onClick: !item.children
|
||||
? () => {
|
||||
const newSearchParams = new URLSearchParams(window.location.search);
|
||||
newSearchParams.set("page", item.page);
|
||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
|
||||
setPage(item.page);
|
||||
}
|
||||
: undefined,
|
||||
}))}
|
||||
items={buildMenuItems()}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
{isAdminRole(userRole) && !collapsed && <UsageIndicator accessToken={accessToken} width={220} />}
|
||||
|
||||
@@ -43,11 +43,28 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
}) => {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
const [logoutUrl, setLogoutUrl] = useState("");
|
||||
const [version, setVersion] = useState("");
|
||||
const { logoUrl } = useTheme();
|
||||
|
||||
// Simple logo URL: use custom logo if available, otherwise default
|
||||
const imageUrl = logoUrl || `${baseUrl}/get_image`;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchVersion = async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/health/readiness`);
|
||||
const data = await response.json();
|
||||
if (data.litellm_version) {
|
||||
setVersion(data.litellm_version);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch version:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchVersion();
|
||||
}, [baseUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const initializeProxySettings = async () => {
|
||||
if (accessToken) {
|
||||
@@ -146,9 +163,21 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Link href="/" className="flex items-center">
|
||||
<img src={imageUrl} alt="LiteLLM Brand" className="h-10 w-auto" />
|
||||
</Link>
|
||||
<div className="flex items-center">
|
||||
<Link href="/" className="flex items-center">
|
||||
<img src={imageUrl} alt="LiteLLM Brand" className="h-10 w-auto" />
|
||||
</Link>
|
||||
{version && (
|
||||
<a
|
||||
href="https://docs.litellm.ai/release_notes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-gray-500 border border-gray-200 rounded-lg px-2 py-0.5 bg-gray-50 font-medium -ml-2 hover:bg-gray-100 transition-colors cursor-pointer z-10"
|
||||
>
|
||||
v{version}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Right side nav items */}
|
||||
<div className="flex items-center space-x-5 ml-auto">
|
||||
|
||||
Reference in New Issue
Block a user