/* Copyright (C) 2017 Cloudbase Solutions SRL This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ // @flow import React from 'react' import { Link } from 'react-router-dom' import { observer } from 'mobx-react' import styled from 'styled-components' import autobind from 'autobind-decorator' import Logo from '../../atoms/Logo' import userStore from '../../../stores/UserStore' import configLoader from '../../../utils/Config' import StyleProps from '../../styleUtils/StyleProps' import { navigationMenu } from '../../../constants' import backgroundImage from './images/star-bg.jpg' import cbsImage from './images/cbsl-logo.svg' import cbsImageSmall from './images/cbsl-logo-small.svg' import tinyLogo from './images/logo-small.svg' import replicaImage from './images/replica-menu.svg' import endpointImage from './images/endpoint-menu.svg' import planningImage from './images/planning-menu.svg' import projectImage from './images/project-menu.svg' import userImage from './images/user-menu.svg' import logsImage from './images/logs-menu.svg' const MENU_MAX_WIDTH_TOGGLE = 1350 const isCollapsed = (props: any) => props.collapsed || (window.outerWidth < MENU_MAX_WIDTH_TOGGLE) const ANIMATION = '200ms' const Wrapper = styled.div` background-image: url('${backgroundImage}'); display: flex; flex-direction: column; align-items: center; height: 100%; width: ${props => isCollapsed(props) ? '80px' : '320px'}; transition: width ${ANIMATION}; ` const LogoWrapper = styled.div` position: relative; height: 48px; margin-top: 48px; width: 100%; display: flex; justify-content: center; ` const LogoStyled = styled(Logo)` position: absolute; top: 0; left: ${props => isCollapsed(props) ? '-9999px' : 'auto'}; cursor: pointer; display: flex; ` const WrappedLink = (props: any) => (
{ props.customRef && props.customRef(r) }} >
) const TinyLogo = styled(WrappedLink)` position: absolute; top: 0; opacity: ${props => isCollapsed(props) ? 1 : 0}; background: url('${tinyLogo}') center no-repeat; display: flex; width: 48px; height: 48px; transition: opacity ${ANIMATION}; ` const MenuWrapper = styled.div` position: relative; display: flex; flex-direction: column; align-items: center; flex-grow: 1; margin-top: 32px; width: 100%; ` const Menu = styled.div` display: flex; flex-direction: column; position: absolute; top: 0; left: ${props => isCollapsed(props) ? '-9999px' : 'auto'}; opacity: ${props => isCollapsed(props) ? 0 : 1}; transition: opacity ${ANIMATION}; ` const MenuItem = styled(Link)` font-size: 18px; color: ${props => props.selected ? '#007AFF' : 'white'}; cursor: pointer; margin-top: 26px; text-decoration: none; width: 145px; margin-left: 32px; ` const SmallMenu = styled.div` display: flex; flex-direction: column; position: absolute; opacity: ${props => isCollapsed(props) ? 1 : 0}; left: ${props => isCollapsed(props) ? 'auto' : '-9999px'}; top: 0; transition: opacity ${ANIMATION}; ` const SmallMenuBackground = styled.div` border-radius: 50%; opacity: 0.15; position: absolute; top: 0; left: 0; right: 0; bottom: 0; transition: background-color ${ANIMATION}; ` const MenuTooltip = styled.div` position: absolute; font-size: 12px; color: #202234; background: #D8DBE2; border-radius: 8px; padding: 1px 6px; white-space: nowrap; transition: opacity ${ANIMATION}; opacity: 0; left: -9999px; ` const SmallMenuItem = styled(Link)` position: relative; cursor: pointer; width: 38px; height: 38px; margin-top: 16px; display: flex; justify-content: center; align-items: center; ${SmallMenuBackground} { background: ${props => props.selected ? 'white' : 'inherit'} } &:hover { ${SmallMenuBackground} { background: white; } ${MenuTooltip} { opacity: 1; left: 42px; } } ` const SmallMenuItemBullet = styled.div` width: 7px; height: 7px; border-radius: 50%; position: absolute; left: -12px; background: ${props => props.bullet === 'replica' ? '#E62565' : '#0044CA'}; ` const MenuImage = styled.div` width: 24px; height: 24px; background: url('${props => props.image}') center no-repeat; background-size: contain; ` const Footer = styled.div` flex-shrink: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; margin-bottom: 32px; ` const CbsLogoWrapper = styled.div` position: relative; display: flex; width: 100%; height: 34px; justify-content: center; ` const CbsLogo = styled.a` position: absolute; top: 0; left: ${props => isCollapsed(props) ? '-9999px' : 'auto'}; opacity: ${props => isCollapsed(props) ? 0 : 1}; width: 128px; height: 34px; background: url('${cbsImage}') center no-repeat; cursor: pointer; display: flex; transition: opacity ${ANIMATION}; ` const CbsLogoSmall = styled.a` position: absolute; top: 0; left: ${props => isCollapsed(props) ? 'auto' : '-9999px'}; opacity: ${props => isCollapsed(props) ? 1 : 0}; width: 48px; height: 34px; background: url('${cbsImageSmall}') center no-repeat; cursor: pointer; display: flex; transition: opacity ${ANIMATION}; ` export const TEST_ID = 'navigation' type Props = { currentPage?: string, className?: string, collapsed?: boolean, hideLogos?: boolean, } @observer class Navigation extends React.Component { wrapper: ?HTMLElement coriolisLogo: ?HTMLElement coriolisLogoSmall: ?HTMLElement cbsLogo: ?HTMLElement cbsLogoSmall: ?HTMLElement menu: ?HTMLElement smallMenu: ?HTMLElement resizeTimeout: ?TimeoutID isCollapsed: boolean = false get filteredMenu() { const isAdmin = userStore.loggedUser ? userStore.loggedUser.isAdmin : false const isDisabled = (page: string) => configLoader.config ? configLoader.config.disabledPages.find(p => p === page) : false return navigationMenu.filter(i => !i.hidden && !isDisabled(i.value) && (!i.requiresAdmin || isAdmin)) } componentDidMount() { if (this.props.collapsed) { return } window.addEventListener('resize', this.handleWindowResize) } componentWillUnmount() { if (this.props.collapsed) { return } window.removeEventListener('resize', this.handleWindowResize) } @autobind handleCollapsedTransitionEnd() { if (!this.coriolisLogo || !this.cbsLogo || !this.menu || !this.isCollapsed) { return } this.coriolisLogo.style.left = '-9999px' this.cbsLogo.style.left = '-9999px' this.menu.style.left = '-9999px' this.cbsLogo.removeEventListener('transitionend', this.handleCollapsedTransitionEnd) } @autobind handleExpandedTransitionEnd() { if (!this.smallMenu || this.isCollapsed || !this.cbsLogoSmall) { return } this.smallMenu.style.left = '-9999px' this.cbsLogoSmall.style.left = '-9999px' this.smallMenu.removeEventListener('transitionend', this.handleExpandedTransitionEnd) } @autobind handleWindowResize() { if (this.resizeTimeout) { return } this.resizeTimeout = setTimeout(() => { this.resizeTimeout = null this.toggleMenu(window.outerWidth < MENU_MAX_WIDTH_TOGGLE) }, 100) } toggleMenu(toCollapsed: boolean) { if ( !this.wrapper || !this.coriolisLogo || !this.coriolisLogoSmall || !this.cbsLogo || !this.cbsLogoSmall || !this.menu || !this.smallMenu ) { return } if (toCollapsed) { this.smallMenu.style.left = 'auto' this.cbsLogoSmall.style.left = 'auto' this.cbsLogo.addEventListener('transitionend', this.handleCollapsedTransitionEnd) } else { this.coriolisLogo.style.left = 'auto' this.cbsLogo.style.left = 'auto' this.menu.style.left = 'auto' this.smallMenu.addEventListener('transitionend', this.handleExpandedTransitionEnd) } this.isCollapsed = toCollapsed this.wrapper.style.width = toCollapsed ? '80px' : '320px' this.coriolisLogoSmall.style.opacity = toCollapsed ? '1' : '0' this.coriolisLogo.style.opacity = toCollapsed ? '0' : '1' this.cbsLogo.style.opacity = toCollapsed ? '0' : '1' this.cbsLogoSmall.style.opacity = toCollapsed ? '1' : '0' this.menu.style.opacity = toCollapsed ? '0' : '1' this.smallMenu.style.opacity = toCollapsed ? '1' : '0' } renderMenu() { return ( { this.menu = ref }} collapsed={this.props.collapsed}> { this.filteredMenu.map(item => { return ( {item.label} ) })} ) } renderSmallMenu() { return ( { this.smallMenu = ref }} collapsed={this.props.collapsed}> { this.filteredMenu.map(item => { let menuImage let bullet switch (item.value) { case 'replicas': bullet = 'replica' menuImage = replicaImage break case 'migrations': bullet = 'migration' menuImage = replicaImage break case 'endpoints': menuImage = endpointImage break case 'planning': menuImage = planningImage break case 'projects': menuImage = projectImage break case 'users': menuImage = userImage break case 'logging': menuImage = logsImage break default: } return ( {bullet ? : null} {item.label} ) })} ) } render() { return ( { this.wrapper = ref }} className={this.props.className} collapsed={this.props.collapsed} > {this.props.hideLogos ? null : ( { this.coriolisLogo = ref }} /> { this.coriolisLogoSmall = ref }} to={navigationMenu[0].value} /> )} {this.renderMenu()} {this.renderSmallMenu()} ) } } export default Navigation