Files
gpustack-ui/src/layouts/header-menu.tsx
T
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 8d99dcd681 feat(layout): move navigation menu from sidebar to top header
Switch ProLayout to top layout with a custom horizontal HeaderMenu
(dropdowns for grouped items) and relocate the user/help/version actions
into the header right area.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-04 14:39:20 +08:00

129 lines
3.1 KiB
TypeScript

import { IconFont } from '@gpustack/core-ui';
import { Link, useLocation, useNavigate } from '@umijs/max';
import { Menu } from 'antd';
import { createStyles } from 'antd-style';
import React, { useMemo } from 'react';
interface MenuItem {
icon?: string;
selectedIcon?: string;
defaultIcon?: string;
children?: MenuItem[];
[key: string]: any;
}
interface HeaderMenuProps {
menuData: MenuItem[];
initialState?: Global.InitialStateType;
}
const useStyles = createStyles(({ css }) => {
return {
headerMenu: css`
flex: 1;
min-width: 0;
background: transparent;
border-bottom: none;
line-height: inherit;
&.ant-menu-horizontal {
border-bottom: none;
}
&.ant-menu-horizontal > .ant-menu-item::after,
&.ant-menu-horizontal > .ant-menu-submenu::after {
display: none;
}
.ant-menu-title-content {
display: inline-flex;
align-items: center;
gap: 8px;
}
.anticon {
font-size: 16px;
}
`
};
});
const isItemSelected = (item: MenuItem, pathname: string) => {
return (
pathname === item.path ||
(Array.isArray(item.subMenu) && item.subMenu.includes(pathname))
);
};
const HeaderMenu: React.FC<HeaderMenuProps> = (props) => {
const { menuData } = props;
const { styles } = useStyles();
const location = useLocation();
const navigate = useNavigate();
const buildLeaf = (item: MenuItem) => {
const selected = isItemSelected(item, location.pathname);
return {
key: item.path as string,
label: (
<Link
prefetch="intent"
to={(item.path as string).replace('/*', '')}
target={item.target}
>
<span className="flex-center gap-8">
<IconFont
type={selected ? item.selectedIcon || '' : item.defaultIcon || ''}
/>
<span>{item.name}</span>
</span>
</Link>
)
};
};
const items = useMemo(() => {
return menuData.map((item) => {
if (item.children && item.children.length > 0) {
return {
key: item.key,
label: item.name,
children: item.children.map((child) => buildLeaf(child))
};
}
return buildLeaf(item);
});
}, [menuData, location.pathname]);
const selectedKeys = useMemo(() => {
const keys: string[] = [];
for (const item of menuData) {
const leaves =
item.children && item.children.length > 0 ? item.children : [item];
for (const leaf of leaves) {
if (isItemSelected(leaf, location.pathname)) {
keys.push(leaf.path as string);
}
}
}
return keys;
}, [menuData, location.pathname]);
const handleClick = ({ key }: { key: string }) => {
if (key.startsWith('/')) {
navigate(key.replace('/*', ''));
}
};
return (
<Menu
className={styles.headerMenu}
mode="horizontal"
selectedKeys={selectedKeys}
items={items}
onClick={handleClick}
triggerSubMenuAction="hover"
/>
);
};
export default HeaderMenu;