| /** |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| |
| import React, { PureComponent } from 'react'; |
| import { Layout, Menu, Icon } from 'antd'; |
| import pathToRegexp from 'path-to-regexp'; |
| import { Link } from 'dva/router'; |
| import styles from './index.less'; |
| |
| const { Sider } = Layout; |
| const { SubMenu } = Menu; |
| |
| // Allow menu.js config icon as string or ReactNode |
| // icon: 'setting', |
| // icon: 'http://demo.com/icon.png', |
| // icon: <Icon type="setting" />, |
| const getIcon = (icon) => { |
| if (typeof icon === 'string' && icon.indexOf('http') === 0) { |
| return <img src={icon} alt="icon" className={styles.icon} />; |
| } |
| if (typeof icon === 'string' && icon.indexOf('/img') === 0) { |
| return <img src={icon} alt="icon" className={styles.icon} />; |
| } |
| if (typeof icon === 'string') { |
| return <Icon type={icon} />; |
| } |
| return icon; |
| }; |
| |
| export default class SiderMenu extends PureComponent { |
| constructor(props) { |
| super(props); |
| this.menus = props.menuData; |
| this.state = { |
| openKeys: this.getDefaultCollapsedSubMenus(props), |
| }; |
| } |
| |
| componentWillReceiveProps(nextProps) { |
| const {...propsData} = this.props; |
| if (nextProps.location.pathname !== propsData.location.pathname) { |
| this.setState({ |
| openKeys: this.getDefaultCollapsedSubMenus(nextProps), |
| }); |
| } |
| } |
| |
| /** |
| * Convert pathname to openKeys |
| * /list/search/articles = > ['list','/list/search'] |
| * @param props |
| */ |
| getDefaultCollapsedSubMenus(props) { |
| const { location: { pathname } } = props || this.props; |
| // eg. /list/search/articles = > ['','list','search','articles'] |
| let snippets = pathname.split('/'); |
| // Delete the end |
| // eg. delete 'articles' |
| snippets.pop(); |
| // Delete the head |
| // eg. delete '' |
| snippets.shift(); |
| // eg. After the operation is completed, the array should be ['list','search'] |
| // eg. Forward the array as ['list','list/search'] |
| snippets = snippets.map((item, index) => { |
| // If the array length > 1 |
| if (index > 0) { |
| // eg. search => ['list','search'].join('/') |
| return snippets.slice(0, index + 1).join('/'); |
| } |
| // index 0 to not do anything |
| return item; |
| }); |
| snippets = snippets.map((item) => { |
| return this.getSelectedMenuKeys(`/${item}`)[0]; |
| }); |
| // eg. ['list','list/search'] |
| return snippets; |
| } |
| |
| /** |
| * Recursively flatten the data |
| * [{path:string},{path:string}] => {path,path2} |
| * @param menus |
| */ |
| getFlatMenuKeys(menus) { |
| let keys = []; |
| menus.forEach((item) => { |
| if (item.children) { |
| keys.push(item.path); |
| keys = keys.concat(this.getFlatMenuKeys(item.children)); |
| } else { |
| keys.push(item.path); |
| } |
| }); |
| return keys; |
| } |
| |
| /** |
| * Get selected child nodes |
| * /user/chen => /user/:id |
| */ |
| getSelectedMenuKeys = (path) => { |
| const flatMenuKeys = this.getFlatMenuKeys(this.menus); |
| return flatMenuKeys.filter((item) => { |
| return pathToRegexp(`/${item}`).test(path); |
| }); |
| } |
| |
| /** |
| * Judge whether it is http link.return a or Link |
| * @memberof SiderMenu |
| */ |
| getMenuItemPath = (item) => { |
| const {...propsData} = this.props; |
| const itemPath = this.conversionPath(item.path); |
| const icon = getIcon(item.icon); |
| const { target, name } = item; |
| // Is it a http link |
| if (/^https?:\/\//.test(itemPath)) { |
| return ( |
| <a href={itemPath} target={target}> |
| {icon}<span>{name}</span> |
| </a> |
| ); |
| } |
| return ( |
| <Link |
| to={itemPath} |
| target={target} |
| replace={itemPath === propsData.location.pathname} |
| onClick={() => propsData.onCollapse(true)} |
| > |
| {icon}<span>{name}</span> |
| </Link> |
| ); |
| } |
| |
| /** |
| * get SubMenu or Item |
| */ |
| getSubMenuOrItem=(item) => { |
| if (item.children && item.children.some(child => child.name)) { |
| return ( |
| <SubMenu |
| title={ |
| item.icon ? ( |
| <span> |
| {getIcon(item.icon)} |
| <span>{item.name}</span> |
| </span> |
| ) : item.name |
| } |
| key={item.path} |
| > |
| {this.getNavMenuItems(item.children)} |
| </SubMenu> |
| ); |
| } else { |
| return ( |
| <Menu.Item key={item.path}> |
| {this.getMenuItemPath(item)} |
| </Menu.Item> |
| ); |
| } |
| } |
| |
| /** |
| * Get subnodes |
| * @memberof SiderMenu |
| */ |
| getNavMenuItems = (menusData) => { |
| if (!menusData) { |
| return []; |
| } |
| return menusData |
| .filter(item => item.name && !item.hideInMenu) |
| .map((item) => { |
| const ItemDom = this.getSubMenuOrItem(item); |
| return this.checkPermissionItem(item.authority, ItemDom); |
| }) |
| .filter(item => !!item); |
| } |
| |
| // conversion Path |
| conversionPath=(path) => { |
| if (path && path.indexOf('http') === 0) { |
| return path; |
| } else { |
| return `/${path || ''}`.replace(/\/+/g, '/'); |
| } |
| } |
| |
| // permission to check |
| checkPermissionItem = (authority, ItemDom) => { |
| const {...propsData} = this.props; |
| if (propsData.Authorized && propsData.Authorized.check) { |
| const { check } = propsData.Authorized; |
| return check( |
| authority, |
| ItemDom |
| ); |
| } |
| return ItemDom; |
| } |
| |
| handleOpenChange = (openKeys) => { |
| const lastOpenKey = openKeys[openKeys.length - 1]; |
| const isMainMenu = this.menus.some( |
| item => lastOpenKey && (item.key === lastOpenKey || item.path === lastOpenKey) |
| ); |
| this.setState({ |
| openKeys: isMainMenu ? [lastOpenKey] : [...openKeys], |
| }); |
| } |
| |
| render() { |
| const { logo, collapsed, location: { pathname }, onCollapse } = this.props; |
| const { openKeys } = this.state; |
| // Don't show popup menu when it is been collapsed |
| const menuProps = collapsed ? {} : { |
| openKeys, |
| }; |
| // if pathname can't match, use the nearest parent's key |
| let selectedKeys = this.getSelectedMenuKeys(pathname); |
| if (!selectedKeys.length) { |
| selectedKeys = [openKeys[openKeys.length - 1]]; |
| } |
| return ( |
| <Sider |
| trigger={null} |
| collapsible |
| collapsed={collapsed} |
| breakpoint="lg" |
| onCollapse={onCollapse} |
| width={256} |
| className={styles.sider} |
| > |
| <div className={styles.logo} key="logo"> |
| <Link to="/"> |
| <img src={logo} alt="logo" /> |
| <h1>Sky Walking</h1> |
| </Link> |
| </div> |
| <Menu |
| key="Menu" |
| theme="dark" |
| mode="inline" |
| {...menuProps} |
| onOpenChange={this.handleOpenChange} |
| selectedKeys={selectedKeys} |
| style={{ padding: '16px 0', width: '100%' }} |
| > |
| {this.getNavMenuItems(this.menus)} |
| </Menu> |
| </Sider> |
| ); |
| } |
| } |