Navigation.jsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. // @flow
  15. import React from 'react'
  16. import { Link } from 'react-router-dom'
  17. import { observer } from 'mobx-react'
  18. import styled from 'styled-components'
  19. import autobind from 'autobind-decorator'
  20. import Logo from '../../atoms/Logo'
  21. import userStore from '../../../stores/UserStore'
  22. import configLoader from '../../../utils/Config'
  23. import StyleProps from '../../styleUtils/StyleProps'
  24. import { navigationMenu } from '../../../constants'
  25. import backgroundImage from './images/star-bg.jpg'
  26. import cbsImage from './images/cbsl-logo.svg'
  27. import cbsImageSmall from './images/cbsl-logo-small.svg'
  28. import tinyLogo from './images/logo-small.svg'
  29. import replicaImage from './images/replica-menu.svg'
  30. import endpointImage from './images/endpoint-menu.svg'
  31. import planningImage from './images/planning-menu.svg'
  32. import projectImage from './images/project-menu.svg'
  33. import userImage from './images/user-menu.svg'
  34. import logsImage from './images/logs-menu.svg'
  35. const MENU_MAX_WIDTH_TOGGLE = 1350
  36. const isCollapsed = (props: any) => props.collapsed || (window.outerWidth < MENU_MAX_WIDTH_TOGGLE)
  37. const ANIMATION = '200ms'
  38. const Wrapper = styled.div`
  39. background-image: url('${backgroundImage}');
  40. display: flex;
  41. flex-direction: column;
  42. align-items: center;
  43. height: 100%;
  44. width: ${props => isCollapsed(props) ? '80px' : '320px'};
  45. transition: width ${ANIMATION};
  46. `
  47. const LogoWrapper = styled.div`
  48. position: relative;
  49. height: 48px;
  50. margin-top: 48px;
  51. width: 100%;
  52. display: flex;
  53. justify-content: center;
  54. `
  55. const LogoStyled = styled(Logo)`
  56. position: absolute;
  57. top: 0;
  58. left: ${props => isCollapsed(props) ? '-9999px' : 'auto'};
  59. cursor: pointer;
  60. display: flex;
  61. `
  62. const WrappedLink = (props: any) => (
  63. <div
  64. style={{ transition: `all ${StyleProps.animations.swift}` }}
  65. className={props.className}
  66. ref={r => { props.customRef && props.customRef(r) }}
  67. >
  68. <Link to={props.to} style={{ display: 'flex', width: '100%' }} />
  69. </div>
  70. )
  71. const TinyLogo = styled(WrappedLink)`
  72. position: absolute;
  73. top: 0;
  74. opacity: ${props => isCollapsed(props) ? 1 : 0};
  75. background: url('${tinyLogo}') center no-repeat;
  76. display: flex;
  77. width: 48px;
  78. height: 48px;
  79. transition: opacity ${ANIMATION};
  80. `
  81. const MenuWrapper = styled.div`
  82. position: relative;
  83. display: flex;
  84. flex-direction: column;
  85. align-items: center;
  86. flex-grow: 1;
  87. margin-top: 32px;
  88. width: 100%;
  89. `
  90. const Menu = styled.div`
  91. display: flex;
  92. flex-direction: column;
  93. position: absolute;
  94. top: 0;
  95. left: ${props => isCollapsed(props) ? '-9999px' : 'auto'};
  96. opacity: ${props => isCollapsed(props) ? 0 : 1};
  97. transition: opacity ${ANIMATION};
  98. `
  99. const MenuItem = styled(Link)`
  100. font-size: 18px;
  101. color: ${props => props.selected ? '#007AFF' : 'white'};
  102. cursor: pointer;
  103. margin-top: 26px;
  104. text-decoration: none;
  105. width: 145px;
  106. margin-left: 32px;
  107. `
  108. const SmallMenu = styled.div`
  109. display: flex;
  110. flex-direction: column;
  111. position: absolute;
  112. opacity: ${props => isCollapsed(props) ? 1 : 0};
  113. left: ${props => isCollapsed(props) ? 'auto' : '-9999px'};
  114. top: 0;
  115. transition: opacity ${ANIMATION};
  116. `
  117. const SmallMenuBackground = styled.div`
  118. border-radius: 50%;
  119. opacity: 0.15;
  120. position: absolute;
  121. top: 0;
  122. left: 0;
  123. right: 0;
  124. bottom: 0;
  125. transition: background-color ${ANIMATION};
  126. `
  127. const MenuTooltip = styled.div`
  128. position: absolute;
  129. font-size: 12px;
  130. color: #202234;
  131. background: #D8DBE2;
  132. border-radius: 8px;
  133. padding: 1px 6px;
  134. white-space: nowrap;
  135. transition: opacity ${ANIMATION};
  136. opacity: 0;
  137. left: -9999px;
  138. `
  139. const SmallMenuItem = styled(Link)`
  140. position: relative;
  141. cursor: pointer;
  142. width: 38px;
  143. height: 38px;
  144. margin-top: 16px;
  145. display: flex;
  146. justify-content: center;
  147. align-items: center;
  148. ${SmallMenuBackground} {
  149. background: ${props => props.selected ? 'white' : 'inherit'}
  150. }
  151. &:hover {
  152. ${SmallMenuBackground} {
  153. background: white;
  154. }
  155. ${MenuTooltip} {
  156. opacity: 1;
  157. left: 42px;
  158. }
  159. }
  160. `
  161. const SmallMenuItemBullet = styled.div`
  162. width: 7px;
  163. height: 7px;
  164. border-radius: 50%;
  165. position: absolute;
  166. left: -12px;
  167. background: ${props => props.bullet === 'replica' ? '#E62565' : '#0044CA'};
  168. `
  169. const MenuImage = styled.div`
  170. width: 24px;
  171. height: 24px;
  172. background: url('${props => props.image}') center no-repeat;
  173. background-size: contain;
  174. `
  175. const Footer = styled.div`
  176. flex-shrink: 1;
  177. display: flex;
  178. flex-direction: column;
  179. justify-content: center;
  180. align-items: center;
  181. margin-bottom: 32px;
  182. `
  183. const CbsLogoWrapper = styled.div`
  184. position: relative;
  185. display: flex;
  186. width: 100%;
  187. height: 34px;
  188. justify-content: center;
  189. `
  190. const CbsLogo = styled.a`
  191. position: absolute;
  192. top: 0;
  193. left: ${props => isCollapsed(props) ? '-9999px' : 'auto'};
  194. opacity: ${props => isCollapsed(props) ? 0 : 1};
  195. width: 128px;
  196. height: 34px;
  197. background: url('${cbsImage}') center no-repeat;
  198. cursor: pointer;
  199. display: flex;
  200. transition: opacity ${ANIMATION};
  201. `
  202. const CbsLogoSmall = styled.a`
  203. position: absolute;
  204. top: 0;
  205. left: ${props => isCollapsed(props) ? 'auto' : '-9999px'};
  206. opacity: ${props => isCollapsed(props) ? 1 : 0};
  207. width: 48px;
  208. height: 34px;
  209. background: url('${cbsImageSmall}') center no-repeat;
  210. cursor: pointer;
  211. display: flex;
  212. transition: opacity ${ANIMATION};
  213. `
  214. export const TEST_ID = 'navigation'
  215. type Props = {
  216. currentPage?: string,
  217. className?: string,
  218. collapsed?: boolean,
  219. hideLogos?: boolean,
  220. }
  221. @observer
  222. class Navigation extends React.Component<Props> {
  223. wrapper: ?HTMLElement
  224. coriolisLogo: ?HTMLElement
  225. coriolisLogoSmall: ?HTMLElement
  226. cbsLogo: ?HTMLElement
  227. cbsLogoSmall: ?HTMLElement
  228. menu: ?HTMLElement
  229. smallMenu: ?HTMLElement
  230. resizeTimeout: ?TimeoutID
  231. isCollapsed: boolean = false
  232. get filteredMenu() {
  233. const isAdmin = userStore.loggedUser ? userStore.loggedUser.isAdmin : false
  234. const isDisabled = (page: string) => configLoader.config ? configLoader.config.disabledPages.find(p => p === page) : false
  235. return navigationMenu.filter(i => !i.hidden && !isDisabled(i.value) && (!i.requiresAdmin || isAdmin))
  236. }
  237. componentDidMount() {
  238. if (this.props.collapsed) {
  239. return
  240. }
  241. window.addEventListener('resize', this.handleWindowResize)
  242. }
  243. componentWillUnmount() {
  244. if (this.props.collapsed) {
  245. return
  246. }
  247. window.removeEventListener('resize', this.handleWindowResize)
  248. }
  249. @autobind
  250. handleCollapsedTransitionEnd() {
  251. if (!this.coriolisLogo || !this.cbsLogo || !this.menu || !this.isCollapsed) {
  252. return
  253. }
  254. this.coriolisLogo.style.left = '-9999px'
  255. this.cbsLogo.style.left = '-9999px'
  256. this.menu.style.left = '-9999px'
  257. this.cbsLogo.removeEventListener('transitionend', this.handleCollapsedTransitionEnd)
  258. }
  259. @autobind
  260. handleExpandedTransitionEnd() {
  261. if (!this.smallMenu || this.isCollapsed || !this.cbsLogoSmall) {
  262. return
  263. }
  264. this.smallMenu.style.left = '-9999px'
  265. this.cbsLogoSmall.style.left = '-9999px'
  266. this.smallMenu.removeEventListener('transitionend', this.handleExpandedTransitionEnd)
  267. }
  268. @autobind
  269. handleWindowResize() {
  270. if (this.resizeTimeout) {
  271. return
  272. }
  273. this.resizeTimeout = setTimeout(() => {
  274. this.resizeTimeout = null
  275. this.toggleMenu(window.outerWidth < MENU_MAX_WIDTH_TOGGLE)
  276. }, 100)
  277. }
  278. toggleMenu(toCollapsed: boolean) {
  279. if (
  280. !this.wrapper ||
  281. !this.coriolisLogo ||
  282. !this.coriolisLogoSmall ||
  283. !this.cbsLogo ||
  284. !this.cbsLogoSmall ||
  285. !this.menu ||
  286. !this.smallMenu
  287. ) {
  288. return
  289. }
  290. if (toCollapsed) {
  291. this.smallMenu.style.left = 'auto'
  292. this.cbsLogoSmall.style.left = 'auto'
  293. this.cbsLogo.addEventListener('transitionend', this.handleCollapsedTransitionEnd)
  294. } else {
  295. this.coriolisLogo.style.left = 'auto'
  296. this.cbsLogo.style.left = 'auto'
  297. this.menu.style.left = 'auto'
  298. this.smallMenu.addEventListener('transitionend', this.handleExpandedTransitionEnd)
  299. }
  300. this.isCollapsed = toCollapsed
  301. this.wrapper.style.width = toCollapsed ? '80px' : '320px'
  302. this.coriolisLogoSmall.style.opacity = toCollapsed ? '1' : '0'
  303. this.coriolisLogo.style.opacity = toCollapsed ? '0' : '1'
  304. this.cbsLogo.style.opacity = toCollapsed ? '0' : '1'
  305. this.cbsLogoSmall.style.opacity = toCollapsed ? '1' : '0'
  306. this.menu.style.opacity = toCollapsed ? '0' : '1'
  307. this.smallMenu.style.opacity = toCollapsed ? '1' : '0'
  308. }
  309. renderMenu() {
  310. return (
  311. <Menu innerRef={ref => { this.menu = ref }} collapsed={this.props.collapsed}>
  312. {
  313. this.filteredMenu.map(item => {
  314. return (
  315. <MenuItem
  316. key={item.value}
  317. selected={this.props.currentPage === item.value}
  318. to={`/${item.value}`}
  319. data-test-id={`navigation-item-${item.value}`}
  320. >{item.label}</MenuItem>
  321. )
  322. })}
  323. </Menu>
  324. )
  325. }
  326. renderSmallMenu() {
  327. return (
  328. <SmallMenu innerRef={ref => { this.smallMenu = ref }} collapsed={this.props.collapsed}>
  329. {
  330. this.filteredMenu.map(item => {
  331. let menuImage
  332. let bullet
  333. switch (item.value) {
  334. case 'replicas':
  335. bullet = 'replica'
  336. menuImage = replicaImage
  337. break
  338. case 'migrations':
  339. bullet = 'migration'
  340. menuImage = replicaImage
  341. break
  342. case 'endpoints':
  343. menuImage = endpointImage
  344. break
  345. case 'planning':
  346. menuImage = planningImage
  347. break
  348. case 'projects':
  349. menuImage = projectImage
  350. break
  351. case 'users':
  352. menuImage = userImage
  353. break
  354. case 'logging':
  355. menuImage = logsImage
  356. break
  357. default:
  358. }
  359. return (
  360. <SmallMenuItem
  361. key={item.value}
  362. selected={this.props.currentPage === item.value}
  363. to={`/${item.value}`}
  364. data-test-id={`${TEST_ID}-smallMenuItem-${item.value}`}
  365. >
  366. <SmallMenuBackground />
  367. {bullet ? <SmallMenuItemBullet bullet={bullet} /> : null}
  368. <MenuImage image={menuImage} />
  369. <MenuTooltip>{item.label}</MenuTooltip>
  370. </SmallMenuItem>
  371. )
  372. })}
  373. </SmallMenu>
  374. )
  375. }
  376. render() {
  377. return (
  378. <Wrapper
  379. innerRef={ref => { this.wrapper = ref }}
  380. className={this.props.className}
  381. collapsed={this.props.collapsed}
  382. >
  383. {this.props.hideLogos ? null : (
  384. <LogoWrapper>
  385. <LogoStyled
  386. small
  387. collapsed={this.props.collapsed}
  388. to={navigationMenu[0].value}
  389. customRef={ref => { this.coriolisLogo = ref }}
  390. />
  391. <TinyLogo
  392. collapsed={this.props.collapsed}
  393. customRef={ref => { this.coriolisLogoSmall = ref }}
  394. to={navigationMenu[0].value}
  395. />
  396. </LogoWrapper>
  397. )}
  398. <MenuWrapper>
  399. {this.renderMenu()}
  400. {this.renderSmallMenu()}
  401. </MenuWrapper>
  402. <Footer>
  403. {this.props.hideLogos ? null : (
  404. <CbsLogoWrapper>
  405. <CbsLogo
  406. innerRef={ref => { this.cbsLogo = ref }}
  407. href="https://cloudbase.it"
  408. target="_blank"
  409. collapsed={this.props.collapsed}
  410. />
  411. <CbsLogoSmall
  412. innerRef={ref => { this.cbsLogoSmall = ref }}
  413. href="https://cloudbase.it"
  414. target="_blank"
  415. collapsed={this.props.collapsed}
  416. />
  417. </CbsLogoWrapper>
  418. )}
  419. </Footer>
  420. </Wrapper>
  421. )
  422. }
  423. }
  424. export default Navigation