diff --git a/react/src/components/BAISider.tsx b/react/src/components/BAISider.tsx index 340626326..56c565d06 100644 --- a/react/src/components/BAISider.tsx +++ b/react/src/components/BAISider.tsx @@ -1,36 +1,39 @@ import Flex from './Flex'; -import { ConfigProvider, Grid, SiderProps, Typography, theme } from 'antd'; +import { ConfigProvider, Grid, SiderProps, theme } from 'antd'; import Sider from 'antd/es/layout/Sider'; +import classNames from 'classnames'; import _ from 'lodash'; -import React from 'react'; +import React, { forwardRef } from 'react'; export interface BAISiderProps extends SiderProps { logoCollapsed?: React.ReactNode; logo?: React.ReactNode; logoTitleCollapsed?: React.ReactNode; logoTitle?: React.ReactNode; - bottomText?: React.ReactNode; } -export const DEFAULT_COLLAPSED_WIDTH = 74; -const BAISider: React.FC = ({ - children, - logo, - logoCollapsed, - logoTitle, - logoTitleCollapsed, - bottomText, - theme: siderTheme, - ...otherProps -}) => { - const { token } = theme.useToken(); - const { Text } = Typography; - const { xs } = Grid.useBreakpoint(); +export const COLLAPSED_SIDER_WIDTH = 74; +export const SIDER_WIDTH = 240; +const BAISider = forwardRef( + ( + { + children, + logo, + logoCollapsed, + logoTitle, + logoTitleCollapsed, + theme: siderTheme, + ...otherProps + }, + ref, + ) => { + const { token } = theme.useToken(); + const { xs } = Grid.useBreakpoint(); - return ( - <> - - - + - -
-
- {logo} -
-
- {logoCollapsed} -
-
- {/*
- - {otherProps.collapsed ? logoTitleCollapsed : logoTitle} - -
*/} -
- {children} - {bottomText && ( - <> - + - - {bottomText} - +
+
+ {logo} +
+
+ {logoCollapsed} +
+
- - )} -
-
- - ); -}; + {children} + + + + + ); + }, +); export default BAISider; diff --git a/react/src/components/MainLayout/MainLayout.tsx b/react/src/components/MainLayout/MainLayout.tsx index ed3764631..b9d59bf85 100644 --- a/react/src/components/MainLayout/MainLayout.tsx +++ b/react/src/components/MainLayout/MainLayout.tsx @@ -1,5 +1,6 @@ import { useCustomThemeConfig } from '../../helper/customThemeConfig'; import { useBAISettingUserState } from '../../hooks/useBAISetting'; +import useKeyboardShortcut from '../../hooks/useKeyboardShortcut'; import { useThemeMode } from '../../hooks/useThemeMode'; import BAIContentWithDrawerArea from '../BAIContentWithDrawerArea'; import BAIErrorBoundary from '../BAIErrorBoundary'; @@ -51,6 +52,18 @@ function MainLayout() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [compactSidebarActive]); + useKeyboardShortcut( + (event) => { + if (event.key === '[') { + event.preventDefault(); + setSideCollapsed((v) => !v); + } + }, + { + skipShortcutOnMetaKey: true, + }, + ); + // const currentDomainName = useCurrentDomainValue(); const { token } = theme.useToken(); const webUIRef = useRef(null); @@ -132,6 +145,9 @@ function MainLayout() { !compactSidebarActive && setSideCollapsed(false); } }} + onCollapse={(collapsed) => { + setSideCollapsed(collapsed); + }} webuiplugins={webUIPlugins} /> diff --git a/react/src/components/MainLayout/WebUISider.tsx b/react/src/components/MainLayout/WebUISider.tsx index 09bc19a3f..b94d4e77d 100644 --- a/react/src/components/MainLayout/WebUISider.tsx +++ b/react/src/components/MainLayout/WebUISider.tsx @@ -2,7 +2,6 @@ import { filterEmptyItem } from '../../helper'; import { useCustomThemeConfig } from '../../helper/customThemeConfig'; import { useSuspendedBackendaiClient, useWebUINavigate } from '../../hooks'; import { useCurrentUserRole } from '../../hooks/backendai'; -import { useThemeMode } from '../../hooks/useThemeMode'; import EndpointsIcon from '../BAIIcons/EndpointsIcon'; import MyEnvironmentsIcon from '../BAIIcons/MyEnvironmentsIcon'; import SessionsIcon from '../BAIIcons/SessionsIcon'; @@ -10,6 +9,7 @@ import BAIMenu from '../BAIMenu'; import BAISider, { BAISiderProps } from '../BAISider'; import Flex from '../Flex'; import ReverseThemeProvider from '../ReverseThemeProvider'; +import SiderToggleButton from '../SiderToggleButton'; import SignoutModal from '../SignoutModal'; import WebUILink from '../WebUILink'; import { PluginPage, WebUIPluginType } from './MainLayout'; @@ -27,18 +27,19 @@ import { ToolOutlined, UserOutlined, } from '@ant-design/icons'; -import { useToggle } from 'ahooks'; +import { useHover, useToggle } from 'ahooks'; import { theme, MenuProps, Typography, ConfigProvider, Divider, + Grid, } from 'antd'; import { ItemType } from 'antd/lib/menu/interface'; import _ from 'lodash'; import { PlayIcon } from 'lucide-react'; -import React, { useContext } from 'react'; +import React, { useContext, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; @@ -50,6 +51,7 @@ type MenuItem = { interface WebUISiderProps extends Pick { webuiplugins?: WebUIPluginType; + onCollapse?: (collapsed: boolean) => void; } const WebUISider: React.FC = (props) => { const { t } = useTranslation(); @@ -76,6 +78,10 @@ const WebUISider: React.FC = (props) => { const [isOpenSignoutModal, { toggle: toggleSignoutModal }] = useToggle(false); + const siderRef = useRef(null); + const isSiderHover = useHover(siderRef); + const gridBreakpoint = Grid.useBreakpoint(); + const generalMenu = filterEmptyItem([ { label: {t('webui.menu.Summary')}, @@ -231,6 +237,7 @@ const WebUISider: React.FC = (props) => { return ( = (props) => { logoTitleCollapsed={ themeConfig?.logo?.logoTitleCollapsed || siteDescription || 'WebUI' } - bottomText={ - props.collapsed ? null : ( - <> + {...props} + > + { + props.onCollapse?.(collapsed); + }} + hidden={!gridBreakpoint.sm || !isSiderHover} + /> + + + {(currentUserRole === 'superadmin' || currentUserRole === 'admin') && ( + + + {!props.collapsed && ( + + {t('webui.menu.Administration')} + + )} + + ), + children: [...adminMenu, ...superAdminMenu], + }, + ] + : currentUserRole === 'admin' + ? [ + { + type: 'group', + label: ( + + {!props.collapsed && ( + + {t('webui.menu.Administration')} + + )} + + ), + children: [...adminMenu], + }, + ] + : [] + } + /> + + )} + + {props.collapsed ? null : ( + +
- + = (props) => { {`${global.packageVersion}.${globalThis.buildNumber}`} - - ) - } - {...props} - > - - {(currentUserRole === 'superadmin' || currentUserRole === 'admin') && ( - - - {!props.collapsed && ( - - {t('webui.menu.Administration')} - - )} - - ), - children: [...adminMenu, ...superAdminMenu], - }, - ] - : currentUserRole === 'admin' - ? [ - { - type: 'group', - label: ( - - {!props.collapsed && ( - - {t('webui.menu.Administration')} - - )} - - ), - children: [...adminMenu], - }, - ] - : [] - } - /> - + + )} ); diff --git a/react/src/components/SiderToggleButton.tsx b/react/src/components/SiderToggleButton.tsx new file mode 100644 index 000000000..ed95a6076 --- /dev/null +++ b/react/src/components/SiderToggleButton.tsx @@ -0,0 +1,88 @@ +import Flex from './Flex'; +import { Button, ConfigProvider, theme, Tooltip, Typography } from 'antd'; +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface SiderToggleButtonProps { + collapsed?: boolean; + buttonTop?: number; + onClick?: (collapsed: boolean) => void; + hidden?: boolean; + // style?: React.CSSProperties; +} +const SiderToggleButton: React.FC = ({ + collapsed = false, + buttonTop, + onClick, + hidden, +}) => { + const { t } = useTranslation(); + + const { token } = theme.useToken(); + return ( + + + + {collapsed ? t('button.Expand') : t('button.Collapse')} + + {'['} + + + } + placement="right" + > +