diff --git a/frontend/packages/core/public/locales/en/common.json b/frontend/packages/core/public/locales/en/common.json index c9e7817c1db9a8367adffdf985c795d77b8586a2..d4480febc4c8e49a256a480bac5e10b6349c77ab 100644 --- a/frontend/packages/core/public/locales/en/common.json +++ b/frontend/packages/core/public/locales/en/common.json @@ -9,8 +9,11 @@ "graph": "Graphs", "high-dimensional": "High Dimensional", "histogram": "Histogram", + "hyper-parameter": "Hyper Parameters", "image": "Image", + "inactive": "Inactive", "loading": "Please wait while loading data", + "more": "More", "next-page": "Next Page", "pr-curve": "PR Curve", "previous-page": "Prev Page", diff --git a/frontend/packages/core/public/locales/zh/common.json b/frontend/packages/core/public/locales/zh/common.json index 5c62b2315d3b2d026ebd90e974c66f660922aec8..df165c5acd92c53932ddf0833bed6d3f63b27cf9 100644 --- a/frontend/packages/core/public/locales/zh/common.json +++ b/frontend/packages/core/public/locales/zh/common.json @@ -9,8 +9,11 @@ "graph": "网络结构", "high-dimensional": "高维数据映射", "histogram": "直方图", + "hyper-parameter": "超参可视化", "image": "图像", + "inactive": "待使用", "loading": "数据载入中,请稍等", + "more": "更多", "next-page": "下一页", "pr-curve": "PR曲线", "previous-page": "上一页", diff --git a/frontend/packages/core/src/components/Aside.tsx b/frontend/packages/core/src/components/Aside.tsx index 6ac899e160b2a4ade2b0811477064697f8dbc8c4..f81bfabf32722f37e9e368c574f5ff740b4cddd9 100644 --- a/frontend/packages/core/src/components/Aside.tsx +++ b/frontend/packages/core/src/components/Aside.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import React, {FunctionComponent} from 'react'; -import {WithStyled, asideWidth, rem, size, transitionProps} from '~/utils/style'; +import React, {FunctionComponent, useCallback, useLayoutEffect, useRef, useState} from 'react'; +import {WithStyled, asideWidth, rem, transitionProps} from '~/utils/style'; import styled from 'styled-components'; @@ -30,11 +30,16 @@ export const AsideSection = styled.section` } `; -const Wrapper = styled.div<{width?: string | number}>` - ${props => size('100%', props.width == null ? asideWidth : props.width)} +const Wrapper = styled.div.attrs<{width: string | number}>(({width}) => ({ + style: { + width: 'number' === typeof width ? `${width}px` : width + } +}))<{width: string | number}>` + height: 100%; overflow: hidden; display: flex; flex-direction: column; + position: relative; > .aside-top { flex: auto; @@ -55,16 +60,107 @@ const Wrapper = styled.div<{width?: string | number}>` box-shadow: 0 -${rem(5)} ${rem(16)} 0 rgba(0, 0, 0, 0.03); padding: ${rem(20)}; } + + > .aside-resize-bar-left, + > .aside-resize-bar-right { + position: absolute; + width: ${rem(8)}; + height: 100%; + top: 0; + cursor: col-resize; + user-select: none; + + &.aside-resize-bar-left { + left: 0; + } + &.aside-resize-bar-right { + right: 0; + } + } `; type AsideProps = { width?: string | number; bottom?: React.ReactNode; + resizable?: 'left' | 'right'; + minWidth?: number; + maxWidth?: number; + onResized?: (width: number) => unknown; }; -const Aside: FunctionComponent = ({width, bottom, className, children}) => { +const Aside: FunctionComponent = ({ + width, + bottom, + resizable, + minWidth, + maxWidth, + onResized, + className, + children +}) => { + const [sideWidth, setSideWidth] = useState>(width ?? asideWidth); + const ref = useRef(null); + const resizing = useRef(false); + const range = useRef({ + min: minWidth ?? null, + max: maxWidth ?? null + }); + + useLayoutEffect(() => { + range.current.min = minWidth ?? null; + }, [minWidth]); + useLayoutEffect(() => { + range.current.max = maxWidth ?? null; + }, [maxWidth]); + + useLayoutEffect(() => { + if (range.current.min == null && ref.current) { + const {width} = ref.current.getBoundingClientRect(); + range.current.min = width; + } + }, []); + + const mousedown = useCallback(() => { + resizing.current = true; + }, []); + + const mousemove = useCallback( + (event: MouseEvent) => { + if (ref.current && resizing.current) { + const clientX = event.clientX; + const {left, right} = ref.current.getBoundingClientRect(); + let w = 0; + if (resizable === 'left') { + w = Math.max(range.current.min ?? 0, right - clientX); + } else if (resizable === 'right') { + w = Math.max(range.current.min ?? 0, clientX - left); + } + w = Math.min(range.current.max ?? document.body.clientWidth / 2, w); + setSideWidth(w); + } + }, + [resizable] + ); + + const mouseup = useCallback(() => { + resizing.current = false; + if (ref.current) { + onResized?.(ref.current.getBoundingClientRect().width); + } + }, [onResized]); + + useLayoutEffect(() => { + document.addEventListener('mousemove', mousemove); + return () => document.removeEventListener('mousemove', mousemove); + }, [mousemove]); + useLayoutEffect(() => { + document.addEventListener('mouseup', mouseup); + return () => document.removeEventListener('mouseup', mouseup); + }, [mouseup]); + return ( - + + {resizable ?
: null}
{children}
{bottom &&
{bottom}
}
diff --git a/frontend/packages/core/src/components/Navbar.tsx b/frontend/packages/core/src/components/Navbar.tsx index 14c14ef69d65d3b1722cdb9478d28b615f3d0339..68f7f1d8cd4d6065d696069b82438a6204760d2c 100644 --- a/frontend/packages/core/src/components/Navbar.tsx +++ b/frontend/packages/core/src/components/Navbar.tsx @@ -14,9 +14,11 @@ * limitations under the License. */ +// cspell:words cimode + import {Link, LinkProps, useLocation} from 'react-router-dom'; import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react'; -import {border, borderRadius, rem, size, transitionProps} from '~/utils/style'; +import {border, borderRadius, rem, size, transitionProps, triangle} from '~/utils/style'; import Icon from '~/components/Icon'; import Language from '~/components/Language'; @@ -28,17 +30,40 @@ import {getApiToken} from '~/utils/fetch'; import logo from '~/assets/images/logo.svg'; import queryString from 'query-string'; import styled from 'styled-components'; -import useNavItems from '~/hooks/useNavItems'; +import useAvailableComponents from '~/hooks/useAvailableComponents'; import {useTranslation} from 'react-i18next'; const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI; const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH; const API_TOKEN_KEY: string = import.meta.env.SNOWPACK_PUBLIC_API_TOKEN_KEY; -interface NavbarItemProps extends Route { +const MAX_ITEM_COUNT_IN_NAVBAR = 5; + +const flatten = (routes: T[]) => { + const result: Omit[] = []; + routes.forEach(route => { + if (route.children) { + result.push(...flatten(route.children)); + } else { + result.push(route); + } + }); + return result; +}; + +interface NavbarItemProps { + active: boolean; + path?: Route['path']; + showDropdownIcon?: boolean; +} + +interface NavbarItemType { + id: string; cid?: string; + name: string; active: boolean; - children?: ({active: boolean} & NonNullable[number])[]; + path?: Route['path']; + children?: NavbarItemType[]; } function appendApiToken(url: string) { @@ -129,10 +154,27 @@ const NavItem = styled.div<{active?: boolean}>` ${props => border('bottom', rem(3), 'solid', props.active ? 'var(--navbar-highlight-color)' : 'transparent')} ${transitionProps('border-bottom')} text-transform: uppercase; + + &.dropdown-icon { + &::after { + content: ''; + display: inline-block; + width: 0; + height: 0; + margin-left: 0.5rem; + vertical-align: middle; + ${triangle({ + pointingDirection: 'bottom', + width: rem(8), + height: rem(5), + foregroundColor: 'currentColor' + })} + } + } } `; -const SubNav = styled.div` +const SubNavWrapper = styled.div` overflow: hidden; border-radius: ${borderRadius}; `; @@ -156,38 +198,65 @@ const NavItemChild = styled.div<{active?: boolean}>` } `; -const NavbarLink: FunctionComponent<{to?: string} & Omit> = ({to, children, ...props}) => { - return ( - - {children} - - ); -}; - -const NavbarItem = React.forwardRef(({id, cid, path, active}, ref) => { - const {t} = useTranslation('common'); - - const name = useMemo(() => (cid ? `${t(id)} - ${t(cid)}` : t(id)), [t, id, cid]); +const NavbarLink: FunctionComponent<{to?: string} & Omit> = ({to, children, ...props}) => ( + + {children} + +); + +// FIXME: why we need to add children type here... that's weird... +const NavbarItem = React.forwardRef( + ({path, active, showDropdownIcon, children}, ref) => { + if (path) { + return ( + + + {children} + + + ); + } - if (path) { return ( - - {name} - + {children} ); } - - return ( - - {name} - - ); -}); +); NavbarItem.displayName = 'NavbarItem'; +const SubNav: FunctionComponent<{ + menu: Omit[]; + active?: boolean; + path?: string; + showDropdownIcon?: boolean; +}> = ({menu, active, path, showDropdownIcon, children}) => ( + + {menu.map(item => ( + + {item.name} + + ))} + + } + > + + {children} + + +); + const Navbar: FunctionComponent = () => { const {t, i18n} = useTranslation('common'); const {pathname} = useLocation(); @@ -202,11 +271,32 @@ const Navbar: FunctionComponent = () => { const currentPath = useMemo(() => pathname.replace(BASE_URI, ''), [pathname]); - const [navItems] = useNavItems(); - const [items, setItems] = useState([]); + const [components, inactiveComponents] = useAvailableComponents(); + + const componentsInNavbar = useMemo(() => components.slice(0, MAX_ITEM_COUNT_IN_NAVBAR), [components]); + const flattenMoreComponents = useMemo(() => flatten(components.slice(MAX_ITEM_COUNT_IN_NAVBAR)), [components]); + const flattenInactiveComponents = useMemo(() => flatten(inactiveComponents), [inactiveComponents]); + const componentsInMoreMenu = useMemo( + () => + flattenMoreComponents.map(item => ({ + ...item, + active: currentPath === item.path + })), + [currentPath, flattenMoreComponents] + ); + const componentsInInactiveMenu = useMemo( + () => + flattenInactiveComponents.map(item => ({ + ...item, + active: currentPath === item.path + })), + [currentPath, flattenInactiveComponents] + ); + + const [navItemsInNavbar, setNavItemsInNavbar] = useState([]); useEffect(() => { - setItems(oldItems => - navItems.map(item => { + setNavItemsInNavbar(oldItems => + componentsInNavbar.map(item => { const children = item.children?.map(child => ({ ...child, active: child.path === currentPath @@ -217,6 +307,7 @@ const Navbar: FunctionComponent = () => { return { ...item, cid: child.id, + name: child.name, path: currentPath, active: true, children @@ -227,6 +318,7 @@ const Navbar: FunctionComponent = () => { return { ...item, ...oldItem, + name: item.children?.find(c => c.id === oldItem.cid)?.name ?? item.name, active: false, children }; @@ -240,7 +332,7 @@ const Navbar: FunctionComponent = () => { }; }) ); - }, [navItems, currentPath]); + }, [componentsInNavbar, currentPath]); return (