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>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
9a223f9565
commit
a90e02a04c
@@ -0,0 +1,128 @@
|
|||||||
|
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;
|
||||||
+21
-57
@@ -22,7 +22,7 @@ import {
|
|||||||
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
||||||
import { useModel } from '@@/plugin-model';
|
import { useModel } from '@@/plugin-model';
|
||||||
import { ProLayout } from '@ant-design/pro-components';
|
import { ProLayout } from '@ant-design/pro-components';
|
||||||
import { CoreUIProvider, IconFont } from '@gpustack/core-ui';
|
import { CoreUIProvider } from '@gpustack/core-ui';
|
||||||
import {
|
import {
|
||||||
Access,
|
Access,
|
||||||
Outlet,
|
Outlet,
|
||||||
@@ -39,18 +39,18 @@ import {
|
|||||||
useNavigate,
|
useNavigate,
|
||||||
type IRoute
|
type IRoute
|
||||||
} from '@umijs/max';
|
} from '@umijs/max';
|
||||||
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
import { ConfigProvider, Modal, theme } from 'antd';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import 'overlayscrollbars/overlayscrollbars.css';
|
import 'overlayscrollbars/overlayscrollbars.css';
|
||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
import { PageContainerInner } from '../pages/_components/page-box';
|
import { PageContainerInner } from '../pages/_components/page-box';
|
||||||
import Exception from './Exception';
|
import Exception from './Exception';
|
||||||
import './Layout.css';
|
import './Layout.css';
|
||||||
import { LogoIcon, SLogoIcon } from './Logo';
|
import { LogoIcon } from './Logo';
|
||||||
import ErrorBoundary from './error-boundary';
|
import ErrorBoundary from './error-boundary';
|
||||||
import { ExtraContent } from './extraRender';
|
import { ExtraContent } from './extraRender';
|
||||||
|
import HeaderMenu from './header-menu';
|
||||||
import { patchRoutes } from './runtime';
|
import { patchRoutes } from './runtime';
|
||||||
import SiderMenu from './sider-menu';
|
|
||||||
|
|
||||||
// Pages that use the page container in the page
|
// Pages that use the page container in the page
|
||||||
const NO_CONTAINER_PAGES = [
|
const NO_CONTAINER_PAGES = [
|
||||||
@@ -133,7 +133,7 @@ const mapRoutes = (routes: IRoute[], role: string) => {
|
|||||||
|
|
||||||
export default (props: any) => {
|
export default (props: any) => {
|
||||||
const [, contextHolder] = Modal.useModal();
|
const [, contextHolder] = Modal.useModal();
|
||||||
const { themeData, setUserSettings, userSettings } = useUserSettings();
|
const { themeData, userSettings } = useUserSettings();
|
||||||
const [userInfo] = useAtom(userAtom);
|
const [userInfo] = useAtom(userAtom);
|
||||||
const [routeCache] = useAtom(routeCacheAtom);
|
const [routeCache] = useAtom(routeCacheAtom);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -238,13 +238,6 @@ export default (props: any) => {
|
|||||||
|
|
||||||
const coreUISlots = useMemo(() => ({ ExtraContent }), []);
|
const coreUISlots = useMemo(() => ({ ExtraContent }), []);
|
||||||
|
|
||||||
const handleToggleCollapse = (e: any) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setUserSettings({
|
|
||||||
...userSettings,
|
|
||||||
collapsed: !userSettings.collapsed
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const newRoutes = filterRoutes(
|
const newRoutes = filterRoutes(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
clientRoutes.filter((route) => route.id === 'max-tabs'),
|
clientRoutes.filter((route) => route.id === 'max-tabs'),
|
||||||
@@ -274,16 +267,19 @@ export default (props: any) => {
|
|||||||
return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string);
|
return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string);
|
||||||
}, [matchedRoute]);
|
}, [matchedRoute]);
|
||||||
|
|
||||||
const collapsed = useMemo(() => {
|
|
||||||
return userSettings.collapsed || false;
|
|
||||||
}, [userSettings.collapsed]);
|
|
||||||
|
|
||||||
const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => {
|
const renderMenuHeader = (logo: React.ReactNode, title: React.ReactNode) => {
|
||||||
return <>{logo}</>;
|
return <>{logo}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => {
|
const headerContentRender = (
|
||||||
return <SiderMenu {...menuProps}></SiderMenu>;
|
headerProps: any,
|
||||||
|
defaultDom: React.ReactNode
|
||||||
|
) => {
|
||||||
|
return <HeaderMenu {...headerProps}></HeaderMenu>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionsRender = () => {
|
||||||
|
return <ExtraContent isDarkTheme={userSettings.isDarkTheme} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPageChange = async (route: any) => {
|
const onPageChange = async (route: any) => {
|
||||||
@@ -337,17 +333,6 @@ export default (props: any) => {
|
|||||||
navigate(pagepath);
|
navigate(pagepath);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCollapse = (value: boolean) => {
|
|
||||||
// only trigger by window resize
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setUserSettings({
|
|
||||||
...userSettings,
|
|
||||||
collapsed: value
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
componentSize="large"
|
componentSize="large"
|
||||||
@@ -408,48 +393,27 @@ export default (props: any) => {
|
|||||||
<DarkMask></DarkMask>
|
<DarkMask></DarkMask>
|
||||||
<ProLayout
|
<ProLayout
|
||||||
fixSiderbar
|
fixSiderbar
|
||||||
fixedHeader={false}
|
fixedHeader
|
||||||
headerRender={false}
|
|
||||||
breadcrumbRender={false}
|
breadcrumbRender={false}
|
||||||
route={route}
|
route={route}
|
||||||
location={location}
|
location={location}
|
||||||
title={userConfig.title}
|
title={userConfig.title}
|
||||||
navTheme={userSettings.theme}
|
navTheme={userSettings.theme}
|
||||||
layout="side"
|
layout="top"
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
paddingBlock: 0,
|
paddingBlock: 0,
|
||||||
paddingInline: 0
|
paddingInline: 0
|
||||||
}}
|
}}
|
||||||
openKeys={false}
|
|
||||||
disableMobile={true}
|
disableMobile={true}
|
||||||
siderWidth={220}
|
|
||||||
menuFooterRender={() => (
|
|
||||||
<Button
|
|
||||||
style={{
|
|
||||||
border: 'none'
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
type={'text'}
|
|
||||||
onClick={handleToggleCollapse}
|
|
||||||
>
|
|
||||||
<IconFont
|
|
||||||
type={collapsed ? 'icon-expand-left' : 'icon-expand-right'}
|
|
||||||
className="font-size-18"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
onCollapse={onCollapse}
|
|
||||||
onMenuHeaderClick={onMenuHeaderClick}
|
onMenuHeaderClick={onMenuHeaderClick}
|
||||||
collapsed={userSettings.collapsed}
|
|
||||||
onPageChange={onPageChange}
|
onPageChange={onPageChange}
|
||||||
formatMessage={formatMessage}
|
formatMessage={formatMessage}
|
||||||
menu={{
|
menu={{
|
||||||
locale: true,
|
locale: true
|
||||||
type: 'group'
|
|
||||||
}}
|
}}
|
||||||
splitMenus={true}
|
logo={<LogoIcon />}
|
||||||
logo={userSettings.collapsed ? <SLogoIcon /> : <LogoIcon />}
|
headerContentRender={headerContentRender}
|
||||||
menuContentRender={menuContentRender}
|
actionsRender={actionsRender}
|
||||||
{...runtimeConfig}
|
{...runtimeConfig}
|
||||||
ErrorBoundary={ErrorBoundary}
|
ErrorBoundary={ErrorBoundary}
|
||||||
>
|
>
|
||||||
@@ -457,7 +421,7 @@ export default (props: any) => {
|
|||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
height: '100vh',
|
height: '100%',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { ExtraContent } from '@/layouts/extraRender';
|
|
||||||
import {
|
import {
|
||||||
PageContainer,
|
PageContainer,
|
||||||
RouteContext,
|
RouteContext,
|
||||||
type PageContainerProps
|
type PageContainerProps
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { useOverlayScroller } from '@gpustack/core-ui';
|
import { useOverlayScroller } from '@gpustack/core-ui';
|
||||||
import { Divider } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useContext, useEffect, useRef } from 'react';
|
import { useContext, useEffect, useRef } from 'react';
|
||||||
import pageBoxCss from './styles/page-box.less';
|
import pageBoxCss from './styles/page-box.less';
|
||||||
@@ -53,13 +51,7 @@ export const PageContainerInner: React.FC<
|
|||||||
{leftContent || pageContext.title}
|
{leftContent || pageContext.title}
|
||||||
</div>
|
</div>
|
||||||
<div className={pageBoxCss.right}>
|
<div className={pageBoxCss.right}>
|
||||||
{rightContent && (
|
{rightContent && <div>{rightContent}</div>}
|
||||||
<div>
|
|
||||||
{rightContent}
|
|
||||||
<Divider orientation="vertical" style={{ margin: '0 16px' }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ExtraContent />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user