/* 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"; 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 "@src/components/ui/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 transferImage from "./images/transfer-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); }} > {props.children}
); 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 === "transfer" ? "#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}; `; 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 = 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 "transfers": bullet = "transfer"; menuImage = transferImage; break; case "deployments": bullet = "deployment"; menuImage = transferImage; 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;