/* 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 . */ import React, { CSSProperties } 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 '@src/components/ui/Logo' import userStore from '@src/stores/UserStore' import configLoader from '@src/utils/Config' import { navigationMenu } from '@src/constants' import { ThemeProps } from '@src/components/Theme' 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' import dashboardImage from './images/dashboard-menu.svg' import minionPoolsImage from './images/minion-pool-menu.svg' import bareMetalServersImage from './images/bare-metal-servers.svg' const isCollapsed = (props: any) => props.collapsed || (window.outerWidth <= ThemeProps.mobileMaxWidth) 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) => (
{ if (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)<{ selected?: boolean | null }>` font-size: 18px; color: ${props => (props.selected ? '#007AFF' : 'white')}; cursor: pointer; margin-top: 26px; text-decoration: none; width: 160px; 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 | null | undefined coriolisLogo: HTMLElement | null | undefined coriolisLogoSmall: HTMLElement | null | undefined cbsLogo: HTMLElement | null | undefined cbsLogoSmall: HTMLElement | null | undefined menu: HTMLElement | null | undefined smallMenu: HTMLElement | null | undefined resizeTimeout: number | null = null isCollapsed: boolean = false componentDidMount() { if (this.props.collapsed) { return } window.addEventListener('resize', this.handleWindowResize) } componentWillUnmount() { if (this.props.collapsed) { return } window.removeEventListener('resize', this.handleWindowResize) } 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 => !isDisabled(i.value) && (!i.requiresAdmin || isAdmin)) } @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 = window.setTimeout(() => { this.resizeTimeout = null this.toggleMenu(window.outerWidth <= ThemeProps.mobileMaxWidth) }, 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 => ( {item.label} )) } ) } renderSmallMenu() { return ( { this.smallMenu = ref }} collapsed={this.props.collapsed} > { this.filteredMenu.map(item => { let menuImage let bullet let style: CSSProperties | null = null switch (item.value) { case 'dashboard': menuImage = dashboardImage style = { width: '19px', height: '19px' } break case 'replicas': bullet = 'replica' menuImage = replicaImage break case 'migrations': bullet = 'migration' menuImage = replicaImage break case 'endpoints': menuImage = endpointImage break case 'minion-pools': menuImage = minionPoolsImage break case 'bare-metal-servers': menuImage = bareMetalServersImage break case 'planning': menuImage = planningImage break case 'projects': menuImage = projectImage break case 'users': menuImage = userImage break case 'logging': menuImage = logsImage style = { width: '22px', height: '22px' } 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