Navigation.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  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. import React, { CSSProperties } from "react";
  15. import { Link } from "react-router";
  16. import { observer } from "mobx-react";
  17. import styled from "styled-components";
  18. import autobind from "autobind-decorator";
  19. import Logo from "@src/components/ui/Logo";
  20. import userStore from "@src/stores/UserStore";
  21. import configLoader from "@src/utils/Config";
  22. import { navigationMenu } from "@src/constants";
  23. import { ThemeProps } from "@src/components/Theme";
  24. import backgroundImage from "@src/components/ui/Images/star-bg.jpg";
  25. import cbsImage from "./images/cbsl-logo.svg";
  26. import cbsImageSmall from "./images/cbsl-logo-small.svg";
  27. import tinyLogo from "./images/logo-small.svg";
  28. import transferImage from "./images/transfer-menu.svg";
  29. import endpointImage from "./images/endpoint-menu.svg";
  30. import planningImage from "./images/planning-menu.svg";
  31. import projectImage from "./images/project-menu.svg";
  32. import userImage from "./images/user-menu.svg";
  33. import logsImage from "./images/logs-menu.svg";
  34. import dashboardImage from "./images/dashboard-menu.svg";
  35. import minionPoolsImage from "./images/minion-pool-menu.svg";
  36. import bareMetalServersImage from "./images/bare-metal-servers.svg";
  37. const isCollapsed = (props: any) =>
  38. props.collapsed || window.outerWidth <= ThemeProps.mobileMaxWidth;
  39. const ANIMATION = "200ms";
  40. const Wrapper = styled.div<any>`
  41. background-image: url("${backgroundImage}");
  42. display: flex;
  43. flex-direction: column;
  44. align-items: center;
  45. height: 100%;
  46. width: ${props => (isCollapsed(props) ? "80px" : "320px")};
  47. transition: width ${ANIMATION};
  48. `;
  49. const LogoWrapper = styled.div<any>`
  50. position: relative;
  51. height: 48px;
  52. margin-top: 48px;
  53. width: 100%;
  54. display: flex;
  55. justify-content: center;
  56. `;
  57. const LogoStyled = styled(Logo)<any>`
  58. position: absolute;
  59. top: 0;
  60. left: ${props => (isCollapsed(props) ? "-9999px" : "auto")};
  61. cursor: pointer;
  62. display: flex;
  63. `;
  64. const WrappedLink = (props: any) => (
  65. <div
  66. style={{ transition: `all ${ThemeProps.animations.swift}` }}
  67. className={props.className}
  68. ref={r => {
  69. if (props.customRef) props.customRef(r);
  70. }}
  71. >
  72. <Link to={`../${props.to}`} style={{ display: "flex", width: "100%" }}>
  73. {props.children}
  74. </Link>
  75. </div>
  76. );
  77. const TinyLogo = styled(WrappedLink)`
  78. position: absolute;
  79. top: 0;
  80. opacity: ${props => (isCollapsed(props) ? 1 : 0)};
  81. background: url("${tinyLogo}") center no-repeat;
  82. display: flex;
  83. width: 48px;
  84. height: 48px;
  85. transition: opacity ${ANIMATION};
  86. `;
  87. const MenuWrapper = styled.div<any>`
  88. position: relative;
  89. display: flex;
  90. flex-direction: column;
  91. align-items: center;
  92. flex-grow: 1;
  93. margin-top: 32px;
  94. width: 100%;
  95. `;
  96. const Menu = styled.div<any>`
  97. display: flex;
  98. flex-direction: column;
  99. position: absolute;
  100. top: 0;
  101. left: ${props => (isCollapsed(props) ? "-9999px" : "auto")};
  102. opacity: ${props => (isCollapsed(props) ? 0 : 1)};
  103. transition: opacity ${ANIMATION};
  104. `;
  105. const MenuItem = styled(Link)<{ selected?: boolean | null }>`
  106. font-size: 18px;
  107. color: ${props => (props.selected ? "#007AFF" : "white")};
  108. cursor: pointer;
  109. margin-top: 26px;
  110. text-decoration: none;
  111. width: 160px;
  112. margin-left: 32px;
  113. `;
  114. const SmallMenu = styled.div<any>`
  115. display: flex;
  116. flex-direction: column;
  117. position: absolute;
  118. opacity: ${props => (isCollapsed(props) ? 1 : 0)};
  119. left: ${props => (isCollapsed(props) ? "auto" : "-9999px")};
  120. top: 0;
  121. transition: opacity ${ANIMATION};
  122. `;
  123. const SmallMenuBackground = styled.div<any>`
  124. border-radius: 50%;
  125. opacity: 0.15;
  126. position: absolute;
  127. top: 0;
  128. left: 0;
  129. right: 0;
  130. bottom: 0;
  131. transition: background-color ${ANIMATION};
  132. `;
  133. const MenuTooltip = styled.div<any>`
  134. position: absolute;
  135. font-size: 12px;
  136. color: #202234;
  137. background: #d8dbe2;
  138. border-radius: 8px;
  139. padding: 1px 6px;
  140. white-space: nowrap;
  141. transition: opacity ${ANIMATION};
  142. opacity: 0;
  143. left: -9999px;
  144. `;
  145. const SmallMenuItem = styled(Link)<any>`
  146. position: relative;
  147. cursor: pointer;
  148. width: 38px;
  149. height: 38px;
  150. margin-top: 16px;
  151. display: flex;
  152. justify-content: center;
  153. align-items: center;
  154. ${SmallMenuBackground} {
  155. background: ${props => (props.selected ? "white" : "inherit")};
  156. }
  157. &:hover {
  158. ${SmallMenuBackground} {
  159. background: white;
  160. }
  161. ${MenuTooltip} {
  162. opacity: 1;
  163. left: 42px;
  164. }
  165. }
  166. `;
  167. const SmallMenuItemBullet = styled.div<any>`
  168. width: 7px;
  169. height: 7px;
  170. border-radius: 50%;
  171. position: absolute;
  172. left: -12px;
  173. background: ${props => (props.bullet === "transfer" ? "#E62565" : "#0044CA")};
  174. `;
  175. const MenuImage = styled.div<any>`
  176. width: 24px;
  177. height: 24px;
  178. background: url("${props => props.image}") center no-repeat;
  179. background-size: contain;
  180. `;
  181. const Footer = styled.div<any>`
  182. flex-shrink: 1;
  183. display: flex;
  184. flex-direction: column;
  185. justify-content: center;
  186. align-items: center;
  187. margin-bottom: 32px;
  188. `;
  189. const CbsLogoWrapper = styled.div<any>`
  190. position: relative;
  191. display: flex;
  192. width: 100%;
  193. height: 34px;
  194. justify-content: center;
  195. `;
  196. const CbsLogo = styled.a<any>`
  197. position: absolute;
  198. top: 0;
  199. left: ${props => (isCollapsed(props) ? "-9999px" : "auto")};
  200. opacity: ${props => (isCollapsed(props) ? 0 : 1)};
  201. width: 128px;
  202. height: 34px;
  203. background: url("${cbsImage}") center no-repeat;
  204. cursor: pointer;
  205. display: flex;
  206. transition: opacity ${ANIMATION};
  207. `;
  208. const CbsLogoSmall = styled.a<any>`
  209. position: absolute;
  210. top: 0;
  211. left: ${props => (isCollapsed(props) ? "auto" : "-9999px")};
  212. opacity: ${props => (isCollapsed(props) ? 1 : 0)};
  213. width: 48px;
  214. height: 34px;
  215. background: url("${cbsImageSmall}") center no-repeat;
  216. cursor: pointer;
  217. display: flex;
  218. transition: opacity ${ANIMATION};
  219. `;
  220. type Props = {
  221. currentPage?: string;
  222. className?: string;
  223. collapsed?: boolean;
  224. hideLogos?: boolean;
  225. };
  226. @observer
  227. class Navigation extends React.Component<Props> {
  228. wrapper: HTMLElement | null | undefined;
  229. coriolisLogo: HTMLElement | null | undefined;
  230. coriolisLogoSmall: HTMLElement | null | undefined;
  231. cbsLogo: HTMLElement | null | undefined;
  232. cbsLogoSmall: HTMLElement | null | undefined;
  233. menu: HTMLElement | null | undefined;
  234. smallMenu: HTMLElement | null | undefined;
  235. resizeTimeout: number | null = null;
  236. isCollapsed = false;
  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. get filteredMenu() {
  250. const isAdmin = userStore.loggedUser ? userStore.loggedUser.isAdmin : false;
  251. const isDisabled = (page: string) =>
  252. configLoader.config
  253. ? configLoader.config.disabledPages.find(p => p === page)
  254. : false;
  255. return navigationMenu.filter(
  256. i => !isDisabled(i.value) && (!i.requiresAdmin || isAdmin),
  257. );
  258. }
  259. @autobind
  260. handleCollapsedTransitionEnd() {
  261. if (
  262. !this.coriolisLogo ||
  263. !this.cbsLogo ||
  264. !this.menu ||
  265. !this.isCollapsed
  266. ) {
  267. return;
  268. }
  269. this.coriolisLogo.style.left = "-9999px";
  270. this.cbsLogo.style.left = "-9999px";
  271. this.menu.style.left = "-9999px";
  272. this.cbsLogo.removeEventListener(
  273. "transitionend",
  274. this.handleCollapsedTransitionEnd,
  275. );
  276. }
  277. @autobind
  278. handleExpandedTransitionEnd() {
  279. if (!this.smallMenu || this.isCollapsed || !this.cbsLogoSmall) {
  280. return;
  281. }
  282. this.smallMenu.style.left = "-9999px";
  283. this.cbsLogoSmall.style.left = "-9999px";
  284. this.smallMenu.removeEventListener(
  285. "transitionend",
  286. this.handleExpandedTransitionEnd,
  287. );
  288. }
  289. @autobind
  290. handleWindowResize() {
  291. if (this.resizeTimeout) {
  292. return;
  293. }
  294. this.resizeTimeout = window.setTimeout(() => {
  295. this.resizeTimeout = null;
  296. this.toggleMenu(window.outerWidth <= ThemeProps.mobileMaxWidth);
  297. }, 100);
  298. }
  299. toggleMenu(toCollapsed: boolean) {
  300. if (
  301. !this.wrapper ||
  302. !this.coriolisLogo ||
  303. !this.coriolisLogoSmall ||
  304. !this.cbsLogo ||
  305. !this.cbsLogoSmall ||
  306. !this.menu ||
  307. !this.smallMenu
  308. ) {
  309. return;
  310. }
  311. if (toCollapsed) {
  312. this.smallMenu.style.left = "auto";
  313. this.cbsLogoSmall.style.left = "auto";
  314. this.cbsLogo.addEventListener(
  315. "transitionend",
  316. this.handleCollapsedTransitionEnd,
  317. );
  318. } else {
  319. this.coriolisLogo.style.left = "auto";
  320. this.cbsLogo.style.left = "auto";
  321. this.menu.style.left = "auto";
  322. this.smallMenu.addEventListener(
  323. "transitionend",
  324. this.handleExpandedTransitionEnd,
  325. );
  326. }
  327. this.isCollapsed = toCollapsed;
  328. this.wrapper.style.width = toCollapsed ? "80px" : "320px";
  329. this.coriolisLogoSmall.style.opacity = toCollapsed ? "1" : "0";
  330. this.coriolisLogo.style.opacity = toCollapsed ? "0" : "1";
  331. this.cbsLogo.style.opacity = toCollapsed ? "0" : "1";
  332. this.cbsLogoSmall.style.opacity = toCollapsed ? "1" : "0";
  333. this.menu.style.opacity = toCollapsed ? "0" : "1";
  334. this.smallMenu.style.opacity = toCollapsed ? "1" : "0";
  335. }
  336. renderMenu() {
  337. return (
  338. <Menu
  339. ref={(ref: HTMLElement | null | undefined) => {
  340. this.menu = ref;
  341. }}
  342. collapsed={this.props.collapsed}
  343. >
  344. {this.filteredMenu.map(item => (
  345. <MenuItem
  346. key={item.value}
  347. selected={this.props.currentPage === item.value}
  348. to={`/${item.value}`}
  349. >
  350. {item.label}
  351. </MenuItem>
  352. ))}
  353. </Menu>
  354. );
  355. }
  356. renderSmallMenu() {
  357. return (
  358. <SmallMenu
  359. ref={(ref: HTMLElement | null | undefined) => {
  360. this.smallMenu = ref;
  361. }}
  362. collapsed={this.props.collapsed}
  363. >
  364. {this.filteredMenu.map(item => {
  365. let menuImage;
  366. let bullet;
  367. let style: CSSProperties | null = null;
  368. switch (item.value) {
  369. case "dashboard":
  370. menuImage = dashboardImage;
  371. style = { width: "19px", height: "19px" };
  372. break;
  373. case "transfers":
  374. bullet = "transfer";
  375. menuImage = transferImage;
  376. break;
  377. case "deployments":
  378. bullet = "deployment";
  379. menuImage = transferImage;
  380. break;
  381. case "endpoints":
  382. menuImage = endpointImage;
  383. break;
  384. case "minion-pools":
  385. menuImage = minionPoolsImage;
  386. break;
  387. case "bare-metal-servers":
  388. menuImage = bareMetalServersImage;
  389. break;
  390. case "planning":
  391. menuImage = planningImage;
  392. break;
  393. case "projects":
  394. menuImage = projectImage;
  395. break;
  396. case "users":
  397. menuImage = userImage;
  398. break;
  399. case "logging":
  400. menuImage = logsImage;
  401. style = { width: "22px", height: "22px" };
  402. break;
  403. default:
  404. }
  405. return (
  406. <SmallMenuItem
  407. key={item.value}
  408. selected={this.props.currentPage === item.value}
  409. to={`/${item.value}`}
  410. >
  411. <SmallMenuBackground />
  412. {bullet ? <SmallMenuItemBullet bullet={bullet} /> : null}
  413. <MenuImage image={menuImage} style={style} />
  414. <MenuTooltip>{item.label}</MenuTooltip>
  415. </SmallMenuItem>
  416. );
  417. })}
  418. </SmallMenu>
  419. );
  420. }
  421. render() {
  422. return (
  423. <Wrapper
  424. ref={(ref: HTMLElement | null | undefined) => {
  425. this.wrapper = ref;
  426. }}
  427. className={this.props.className}
  428. collapsed={this.props.collapsed}
  429. >
  430. {this.props.hideLogos ? null : (
  431. <LogoWrapper>
  432. <LogoStyled
  433. small
  434. collapsed={this.props.collapsed}
  435. to={navigationMenu[0].value}
  436. customRef={(ref: HTMLElement | null | undefined) => {
  437. this.coriolisLogo = ref;
  438. }}
  439. />
  440. <TinyLogo
  441. collapsed={this.props.collapsed}
  442. customRef={(ref: HTMLElement | null | undefined) => {
  443. this.coriolisLogoSmall = ref;
  444. }}
  445. to={navigationMenu[0].value}
  446. />
  447. </LogoWrapper>
  448. )}
  449. <MenuWrapper>
  450. {this.renderMenu()}
  451. {this.renderSmallMenu()}
  452. </MenuWrapper>
  453. <Footer>
  454. {this.props.hideLogos ? null : (
  455. <CbsLogoWrapper>
  456. <CbsLogo
  457. ref={(ref: HTMLElement | null | undefined) => {
  458. this.cbsLogo = ref;
  459. }}
  460. href="https://cloudbase.it"
  461. target="_blank"
  462. collapsed={this.props.collapsed}
  463. />
  464. <CbsLogoSmall
  465. ref={(ref: HTMLElement | null | undefined) => {
  466. this.cbsLogoSmall = ref;
  467. }}
  468. href="https://cloudbase.it"
  469. target="_blank"
  470. collapsed={this.props.collapsed}
  471. />
  472. </CbsLogoWrapper>
  473. )}
  474. </Footer>
  475. </Wrapper>
  476. );
  477. }
  478. }
  479. export default Navigation;