summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-05-12 22:42:12 +0200
committerFabian Mastenbroek <mail.fabianm@gmail.com>2021-05-17 17:06:47 +0200
commit4397a959e806bf476be4c81bc804616adf58b969 (patch)
treea2bfdcb314594433413a235b516d18fa5416bf6d /opendc-web/opendc-web-ui/src
parentd21606bd238702645690586df5ad5b5075ca09c9 (diff)
ui: Migrate from CRA to Next.js
This change updates the web frontend to use Next.js instead of Create React App (CRA). Next.js enables the possibility of rendering pages on the server side (which reduces the time to first frame) and overall provides a better development experience. Future commits will try to futher optimize the implementation for Next.js.
Diffstat (limited to 'opendc-web/opendc-web-ui/src')
-rw-r--r--opendc-web/opendc-web-ui/src/actions/map.js1
-rw-r--r--opendc-web/opendc-web-ui/src/actions/modals/profile.js14
-rw-r--r--opendc-web/opendc-web-ui/src/api/index.js31
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/token-signin.js4
-rw-r--r--opendc-web/opendc-web-ui/src/auth/hook.js (renamed from opendc-web/opendc-web-ui/src/config.js)32
-rw-r--r--opendc-web/opendc-web-ui/src/auth/index.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js118
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js13
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js13
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js48
-rw-r--r--opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js20
-rw-r--r--opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js3
-rw-r--r--opendc-web/opendc-web-ui/src/components/navigation/Navbar.js47
-rw-r--r--opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js8
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js8
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/App.js132
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js4
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js6
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js6
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js8
-rw-r--r--opendc-web/opendc-web-ui/src/containers/auth/Login.js5
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js27
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js2
-rw-r--r--opendc-web/opendc-web-ui/src/index.js29
-rw-r--r--opendc-web/opendc-web-ui/src/index.scss2
-rw-r--r--opendc-web/opendc-web-ui/src/pages/404.js19
-rw-r--r--opendc-web/opendc-web-ui/src/pages/404.module.scss (renamed from opendc-web/opendc-web-ui/src/pages/NotFound.module.scss)0
-rw-r--r--opendc-web/opendc-web-ui/src/pages/App.js103
-rw-r--r--opendc-web/opendc-web-ui/src/pages/NotFound.js15
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Profile.js31
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Projects.js30
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_app.js42
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_document.js96
-rw-r--r--opendc-web/opendc-web-ui/src/pages/index.js (renamed from opendc-web/opendc-web-ui/src/pages/Home.js)12
-rw-r--r--opendc-web/opendc-web-ui/src/pages/index.module.scss (renamed from opendc-web/opendc-web-ui/src/pages/Home.module.scss)0
-rw-r--r--opendc-web/opendc-web-ui/src/pages/profile.js54
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js (renamed from opendc-web/opendc-web-ui/src/util/hooks.js)18
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js37
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/index.js36
-rw-r--r--opendc-web/opendc-web-ui/src/reducers/modals.js2
-rw-r--r--opendc-web/opendc-web-ui/src/routes/index.js40
-rw-r--r--opendc-web/opendc-web-ui/src/store/configure-store.js51
-rw-r--r--opendc-web/opendc-web-ui/src/util/sidebar-space.js4
43 files changed, 688 insertions, 485 deletions
diff --git a/opendc-web/opendc-web-ui/src/actions/map.js b/opendc-web/opendc-web-ui/src/actions/map.js
index 0d49d849..09196dca 100644
--- a/opendc-web/opendc-web-ui/src/actions/map.js
+++ b/opendc-web/opendc-web-ui/src/actions/map.js
@@ -64,6 +64,7 @@ export function setMapPositionWithBoundsCheck(x, y) {
const state = getState()
const scaledMapSize = MAP_SIZE_IN_PIXELS * state.map.scale
+
const updatedX =
x > 0
? 0
diff --git a/opendc-web/opendc-web-ui/src/actions/modals/profile.js b/opendc-web/opendc-web-ui/src/actions/modals/profile.js
deleted file mode 100644
index 39c72c03..00000000
--- a/opendc-web/opendc-web-ui/src/actions/modals/profile.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export const OPEN_DELETE_PROFILE_MODAL = 'OPEN_DELETE_PROFILE_MODAL'
-export const CLOSE_DELETE_PROFILE_MODAL = 'CLOSE_DELETE_PROFILE_MODAL'
-
-export function openDeleteProfileModal() {
- return {
- type: OPEN_DELETE_PROFILE_MODAL,
- }
-}
-
-export function closeDeleteProfileModal() {
- return {
- type: CLOSE_DELETE_PROFILE_MODAL,
- }
-}
diff --git a/opendc-web/opendc-web-ui/src/api/index.js b/opendc-web/opendc-web-ui/src/api/index.js
index e6528fd9..65358745 100644
--- a/opendc-web/opendc-web-ui/src/api/index.js
+++ b/opendc-web/opendc-web-ui/src/api/index.js
@@ -1,8 +1,35 @@
-import config from '../config'
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
import { getAuthToken } from '../auth'
-const apiUrl = config['API_BASE_URL']
+const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL
+/**
+ * Send the specified request to the OpenDC API.
+ * @param path Relative path for the API.
+ * @param method The method to use for the request.
+ * @param body The body of the request.
+ */
export async function request(path, method = 'GET', body) {
const res = await fetch(`${apiUrl}/v2/${path}`, {
method: method,
diff --git a/opendc-web/opendc-web-ui/src/api/routes/token-signin.js b/opendc-web/opendc-web-ui/src/api/routes/token-signin.js
index ced5d2e0..1c285bdb 100644
--- a/opendc-web/opendc-web-ui/src/api/routes/token-signin.js
+++ b/opendc-web/opendc-web-ui/src/api/routes/token-signin.js
@@ -1,7 +1,5 @@
-import config from '../../config'
-
export function performTokenSignIn(token) {
- const apiUrl = config['API_BASE_URL']
+ const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL
return fetch(`${apiUrl}/tokensignin`, {
method: 'POST',
diff --git a/opendc-web/opendc-web-ui/src/config.js b/opendc-web/opendc-web-ui/src/auth/hook.js
index 13f4abf2..ddaf53ed 100644
--- a/opendc-web/opendc-web-ui/src/config.js
+++ b/opendc-web/opendc-web-ui/src/auth/hook.js
@@ -20,21 +20,25 @@
* SOFTWARE.
*/
-function getConfig(name) {
- if (process.env.NODE_ENV === 'production' && window.config_overrides) {
- const value = window.config_overrides[name]
- if (value !== `$${name}`) {
- return value
- }
- }
+import { useEffect, useState } from 'react'
+import { userIsLoggedIn } from './index'
+import { useRouter } from 'next/router'
- return process.env[name]
-}
+export function useAuth() {
+ const [isLoggedIn, setLoggedIn] = useState(false)
+
+ useEffect(() => {
+ setLoggedIn(userIsLoggedIn())
+ }, [])
-const config = {
- API_BASE_URL: getConfig('REACT_APP_API_BASE_URL'),
- OAUTH_CLIENT_ID: getConfig('REACT_APP_OAUTH_CLIENT_ID'),
- SENTRY_DSN: getConfig('REACT_APP_SENTRY_DSN'),
+ return isLoggedIn
}
-export default config
+export function useRequireAuth() {
+ const router = useRouter()
+ useEffect(() => {
+ if (!userIsLoggedIn()) {
+ router.replace('/')
+ }
+ })
+}
diff --git a/opendc-web/opendc-web-ui/src/auth/index.js b/opendc-web/opendc-web-ui/src/auth/index.js
index b5953990..148e2e13 100644
--- a/opendc-web/opendc-web-ui/src/auth/index.js
+++ b/opendc-web/opendc-web-ui/src/auth/index.js
@@ -2,7 +2,7 @@ import { LOG_IN_SUCCEEDED, LOG_OUT } from '../actions/auth'
import { DELETE_CURRENT_USER_SUCCEEDED } from '../actions/users'
const getAuthObject = () => {
- const authItem = localStorage.getItem('auth')
+ const authItem = global.localStorage && localStorage.getItem('auth')
if (!authItem || authItem === '{}') {
return undefined
}
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js
index 7ca10792..dd32927f 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { useEffect, useMemo, useRef, useState } from 'react'
import { HotKeys } from 'react-hotkeys'
import { Stage } from 'react-konva'
import MapLayer from '../../../containers/app/map/layers/MapLayer'
@@ -6,85 +6,75 @@ import ObjectHoverLayer from '../../../containers/app/map/layers/ObjectHoverLaye
import RoomHoverLayer from '../../../containers/app/map/layers/RoomHoverLayer'
import { NAVBAR_HEIGHT } from '../../navigation/Navbar'
import { MAP_MOVE_PIXELS_PER_EVENT } from './MapConstants'
-import { Provider } from 'react-redux'
-import { store } from '../../../store/configure-store'
+import { Provider, useStore } from 'react-redux'
-class MapStageComponent extends React.Component {
- state = {
- mouseX: 0,
- mouseY: 0,
+function MapStageComponent({
+ mapDimensions,
+ mapPosition,
+ setMapDimensions,
+ setMapPositionWithBoundsCheck,
+ zoomInOnPosition,
+}) {
+ const [pos, setPos] = useState([0, 0])
+ const stage = useRef(null)
+ const [x, y] = pos
+ const handlers = {
+ MOVE_LEFT: () => moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0),
+ MOVE_RIGHT: () => moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0),
+ MOVE_UP: () => moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT),
+ MOVE_DOWN: () => moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT),
}
- constructor(props) {
- super(props)
+ const moveWithDelta = (deltaX, deltaY) =>
+ setMapPositionWithBoundsCheck(mapPosition.x + deltaX, mapPosition.y + deltaY)
+ const updateMousePosition = () => {
+ if (!stage.current) {
+ return
+ }
- this.updateDimensions = this.updateDimensions.bind(this)
- this.updateScale = this.updateScale.bind(this)
+ const mousePos = stage.current.getStage().getPointerPosition()
+ setPos([mousePos.x, mousePos.y])
}
+ const updateDimensions = () => setMapDimensions(window.innerWidth, window.innerHeight - NAVBAR_HEIGHT)
+ const updateScale = (e) => zoomInOnPosition(e.deltaY < 0, x, y)
- componentDidMount() {
- this.updateDimensions()
+ useEffect(() => {
+ updateDimensions()
- window.addEventListener('resize', this.updateDimensions)
- window.addEventListener('wheel', this.updateScale)
+ window.addEventListener('resize', updateDimensions)
+ window.addEventListener('wheel', updateScale)
window['exportCanvasToImage'] = () => {
const download = document.createElement('a')
- download.href = this.stage.getStage().toDataURL()
+ download.href = stage.current.getStage().toDataURL()
download.download = 'opendc-canvas-export-' + Date.now() + '.png'
download.click()
}
- }
-
- componentWillUnmount() {
- window.removeEventListener('resize', this.updateDimensions)
- window.removeEventListener('wheel', this.updateScale)
- }
-
- updateDimensions() {
- this.props.setMapDimensions(window.innerWidth, window.innerHeight - NAVBAR_HEIGHT)
- }
-
- updateScale(e) {
- this.props.zoomInOnPosition(e.deltaY < 0, this.state.mouseX, this.state.mouseY)
- }
-
- updateMousePosition() {
- const mousePos = this.stage.getStage().getPointerPosition()
- this.setState({ mouseX: mousePos.x, mouseY: mousePos.y })
- }
- handlers = {
- MOVE_LEFT: () => this.moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0),
- MOVE_RIGHT: () => this.moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0),
- MOVE_UP: () => this.moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT),
- MOVE_DOWN: () => this.moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT),
- }
+ return () => {
+ window.removeEventListener('resize', updateDimensions)
+ window.removeEventListener('wheel', updateScale)
+ }
+ }, [])
- moveWithDelta(deltaX, deltaY) {
- this.props.setMapPositionWithBoundsCheck(this.props.mapPosition.x + deltaX, this.props.mapPosition.y + deltaY)
- }
+ const store = useStore()
- render() {
- return (
- <HotKeys handlers={this.handlers}>
- <Stage
- ref={(stage) => {
- this.stage = stage
- }}
- width={this.props.mapDimensions.width}
- height={this.props.mapDimensions.height}
- onMouseMove={this.updateMousePosition.bind(this)}
- >
- <Provider store={store}>
- <MapLayer />
- <RoomHoverLayer mouseX={this.state.mouseX} mouseY={this.state.mouseY} />
- <ObjectHoverLayer mouseX={this.state.mouseX} mouseY={this.state.mouseY} />
- </Provider>
- </Stage>
- </HotKeys>
- )
- }
+ return (
+ <HotKeys handlers={handlers} allowChanges={true}>
+ <Stage
+ ref={stage}
+ width={mapDimensions.width}
+ height={mapDimensions.height}
+ onMouseMove={updateMousePosition}
+ >
+ <Provider store={store}>
+ <MapLayer />
+ <RoomHoverLayer mouseX={x} mouseY={y} />
+ <ObjectHoverLayer mouseX={x} mouseY={y} />
+ </Provider>
+ </Stage>
+ </HotKeys>
+ )
}
export default MapStageComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js
index b000b9e2..b714a7d2 100644
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types'
import React from 'react'
import Shapes from '../../../../shapes'
-import { Link } from 'react-router-dom'
+import Link from 'next/link'
import FontAwesome from 'react-fontawesome'
import ScenarioListContainer from '../../../../containers/app/sidebars/project/ScenarioListContainer'
@@ -44,11 +44,12 @@ class PortfolioListComponent extends React.Component {
{portfolio.name}
</div>
<div className="col-5 text-right">
- <Link
- className="btn btn-outline-primary mr-1 fa fa-play"
- to={`/projects/${this.props.currentProjectId}/portfolios/${portfolio._id}`}
- onClick={() => this.props.onChoosePortfolio(portfolio._id)}
- />
+ <Link href={`/projects/${this.props.currentProjectId}/portfolios/${portfolio._id}`}>
+ <a
+ className="btn btn-outline-primary mr-1 fa fa-play"
+ onClick={() => this.props.onChoosePortfolio(portfolio._id)}
+ />
+ </Link>
<span
className="btn btn-outline-danger fa fa-trash"
onClick={() => this.onDelete(portfolio._id)}
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js
index e775a663..4efa99ef 100644
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types'
import React from 'react'
import Shapes from '../../../../shapes'
-import { Link } from 'react-router-dom'
+import Link from 'next/link'
import FontAwesome from 'react-fontawesome'
class ScenarioListComponent extends React.Component {
@@ -34,10 +34,13 @@ class ScenarioListComponent extends React.Component {
</div>
<div className="col-5 text-right">
<Link
- className="btn btn-outline-primary mr-1 fa fa-play disabled"
- to={`/projects/${this.props.currentProjectId}/portfolios/${scenario.portfolioId}/scenarios/${scenario._id}`}
- onClick={() => this.props.onChooseScenario(scenario.portfolioId, scenario._id)}
- />
+ href={`/projects/${this.props.currentProjectId}/portfolios/${scenario.portfolioId}/scenarios/${scenario._id}`}
+ >
+ <a
+ className="btn btn-outline-primary mr-1 fa fa-play disabled"
+ onClick={() => this.props.onChooseScenario(scenario.portfolioId, scenario._id)}
+ />
+ </Link>
<span
className={'btn btn-outline-danger fa fa-trash ' + (idx === 0 ? 'disabled' : '')}
onClick={() => (idx !== 0 ? this.onDelete(scenario._id) : undefined)}
diff --git a/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js b/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js
index 589047dc..5a95810a 100644
--- a/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js
+++ b/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js
@@ -2,36 +2,26 @@ import PropTypes from 'prop-types'
import React from 'react'
import Modal from './Modal'
-class ConfirmationModal extends React.Component {
- static propTypes = {
- title: PropTypes.string.isRequired,
- message: PropTypes.string.isRequired,
- show: PropTypes.bool.isRequired,
- callback: PropTypes.func.isRequired,
- }
-
- onConfirm() {
- this.props.callback(true)
- }
-
- onCancel() {
- this.props.callback(false)
- }
+function ConfirmationModal({ title, message, show, callback }) {
+ return (
+ <Modal
+ title={title}
+ show={show}
+ onSubmit={() => callback(true)}
+ onCancel={() => callback(false)}
+ submitButtonType="danger"
+ submitButtonText="Confirm"
+ >
+ {message}
+ </Modal>
+ )
+}
- render() {
- return (
- <Modal
- title={this.props.title}
- show={this.props.show}
- onSubmit={this.onConfirm.bind(this)}
- onCancel={this.onCancel.bind(this)}
- submitButtonType="danger"
- submitButtonText="Confirm"
- >
- {this.props.message}
- </Modal>
- )
- }
+ConfirmationModal.propTypes = {
+ title: PropTypes.string.isRequired,
+ message: PropTypes.string.isRequired,
+ show: PropTypes.bool.isRequired,
+ callback: PropTypes.func.isRequired,
}
export default ConfirmationModal
diff --git a/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js b/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js
index 8c28c542..7b1eaae2 100644
--- a/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js
@@ -1,6 +1,6 @@
import React from 'react'
import FontAwesome from 'react-fontawesome'
-import { Link } from 'react-router-dom'
+import Link from 'next/link'
import { NavLink } from 'reactstrap'
import Navbar, { NavItem } from './Navbar'
import {} from './Navbar.module.scss'
@@ -8,16 +8,20 @@ import {} from './Navbar.module.scss'
const AppNavbarComponent = ({ project, fullWidth }) => (
<Navbar fullWidth={fullWidth}>
<NavItem route="/projects">
- <NavLink tag={Link} title="My Projects" to="/projects">
- <FontAwesome name="list" className="mr-2" />
- My Projects
- </NavLink>
+ <Link href="/projects">
+ <NavLink title="My Projects">
+ <FontAwesome name="list" className="mr-2" />
+ My Projects
+ </NavLink>
+ </Link>
</NavItem>
{project ? (
<NavItem>
- <NavLink tag={Link} title="Current Project" to={`/projects/${project._id}`}>
- <span>{project.name}</span>
- </NavLink>
+ <Link href={`/projects/${project._id}`}>
+ <NavLink title="Current Project">
+ <span>{project.name}</span>
+ </NavLink>
+ </Link>
</NavItem>
) : undefined}
</Navbar>
diff --git a/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js b/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js
index 78b02b44..0c0feeb1 100644
--- a/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js
+++ b/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js
@@ -1,11 +1,10 @@
import PropTypes from 'prop-types'
import React from 'react'
import FontAwesome from 'react-fontawesome'
-import { Link } from 'react-router-dom'
import { NavLink } from 'reactstrap'
const LogoutButton = ({ onLogout }) => (
- <NavLink tag={Link} className="logout" title="Sign out" to="#" onClick={onLogout}>
+ <NavLink className="logout" title="Sign out" onClick={onLogout}>
<FontAwesome name="power-off" size="lg" />
</NavLink>
)
diff --git a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js
index bf18f1c4..90f55665 100644
--- a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js
+++ b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js
@@ -1,5 +1,6 @@
import React, { useState } from 'react'
-import { Link, useLocation } from 'react-router-dom'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
import {
Navbar as RNavbar,
NavItem as RNavItem,
@@ -15,6 +16,7 @@ import Login from '../../containers/auth/Login'
import Logout from '../../containers/auth/Logout'
import ProfileName from '../../containers/auth/ProfileName'
import { login, navbar, opendcBrand } from './Navbar.module.scss'
+import { useAuth } from '../../auth/hook'
export const NAVBAR_HEIGHT = 60
@@ -24,32 +26,45 @@ const GitHubLink = () => (
className="ml-2 mr-3 text-dark"
style={{ position: 'relative', top: 7 }}
>
- <span className="fa fa-github fa-2x" />
+ <span aria-hidden className="fa fa-github fa-2x" />
</a>
)
export const NavItem = ({ route, children }) => {
- const location = useLocation()
- return <RNavItem active={location.pathname === route}>{children}</RNavItem>
+ const router = useRouter()
+ const handleClick = (e) => {
+ e.preventDefault()
+ router.push(route)
+ }
+ return (
+ <RNavItem onClick={handleClick} active={router.asPath === route}>
+ {children}
+ </RNavItem>
+ )
}
export const LoggedInSection = () => {
- const location = useLocation()
+ const router = useRouter()
+ const isLoggedIn = useAuth()
return (
<Nav navbar className="auth-links">
- {userIsLoggedIn() ? (
+ {isLoggedIn ? (
[
- location.pathname === '/' ? (
+ router.asPath === '/' ? (
<NavItem route="/projects" key="projects">
- <NavLink tag={Link} title="My Projects" to="/projects">
- My Projects
- </NavLink>
+ <Link href="/projects">
+ <NavLink title="My Projects" to="/projects">
+ My Projects
+ </NavLink>
+ </Link>
</NavItem>
) : (
<NavItem route="/profile" key="profile">
- <NavLink tag={Link} title="My Profile" to="/profile">
- <ProfileName />
- </NavLink>
+ <Link href="/profile">
+ <NavLink title="My Profile">
+ <ProfileName />
+ </NavLink>
+ </Link>
</NavItem>
),
<NavItem route="logout" key="logout">
@@ -57,10 +72,10 @@ export const LoggedInSection = () => {
</NavItem>,
]
) : (
- <NavItem route="login">
+ <RNavItem>
<GitHubLink />
<Login visible={true} className={login} />
- </NavItem>
+ </RNavItem>
)}
</Nav>
)
@@ -74,7 +89,7 @@ const Navbar = ({ fullWidth, children }) => {
<RNavbar fixed="top" color="light" light expand="lg" id="navbar" className={navbar}>
<Container fluid={fullWidth}>
<NavbarToggler onClick={toggle} />
- <NavbarBrand tag={Link} to="/" title="OpenDC" className={opendcBrand}>
+ <NavbarBrand href="/" title="OpenDC" className={opendcBrand}>
<img src="/img/logo.png" alt="OpenDC" />
</NavbarBrand>
diff --git a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js
index b38fc183..28fd81e9 100644
--- a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js
+++ b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js
@@ -1,5 +1,5 @@
import React from 'react'
-import { Link } from 'react-router-dom'
+import Link from 'next/link'
import BlinkingCursor from './BlinkingCursor'
import CodeBlock from './CodeBlock'
import { terminalWindow, terminalHeader, terminalBody, segfault, subTitle, homeBtn } from './TerminalWindow.module.scss'
@@ -23,8 +23,10 @@ const TerminalWindow = () => (
Got lost?
<BlinkingCursor />
</div>
- <Link to="/" className={homeBtn}>
- <span className="fa fa-home" /> GET ME BACK TO OPENDC
+ <Link href="/">
+ <a className={homeBtn}>
+ <span className="fa fa-home" /> GET ME BACK TO OPENDC
+ </a>
</Link>
</div>
</div>
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js
index 1c76cc7f..48cce019 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js
@@ -1,11 +1,13 @@
import PropTypes from 'prop-types'
import React from 'react'
-import { Link } from 'react-router-dom'
+import Link from 'next/link'
const ProjectActionButtons = ({ projectId, onViewUsers, onDelete }) => (
<td className="text-right">
- <Link to={'/projects/' + projectId} className="btn btn-outline-primary btn-sm mr-2" title="Open this project">
- <span className="fa fa-play" />
+ <Link href={`/projects/${projectId}`}>
+ <a className="btn btn-outline-primary btn-sm mr-2" title="Open this project">
+ <span className="fa fa-play" />
+ </a>
</Link>
<div
className="btn btn-outline-success btn-sm disabled mr-2"
diff --git a/opendc-web/opendc-web-ui/src/containers/app/App.js b/opendc-web/opendc-web-ui/src/containers/app/App.js
new file mode 100644
index 00000000..df159cc2
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/App.js
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React, { useEffect } from 'react'
+import Head from 'next/head'
+import { useRouter } from 'next/router'
+import { HotKeys } from 'react-hotkeys'
+import { useDispatch, useSelector } from 'react-redux'
+import { openPortfolioSucceeded } from '../../actions/portfolios'
+import { openProjectSucceeded } from '../../actions/projects'
+import ToolPanelComponent from '../../components/app/map/controls/ToolPanelComponent'
+import LoadingScreen from '../../components/app/map/LoadingScreen'
+import ScaleIndicatorContainer from '../../containers/app/map/controls/ScaleIndicatorContainer'
+import MapStage from '../../containers/app/map/MapStage'
+import TopologySidebarContainer from '../../containers/app/sidebars/topology/TopologySidebarContainer'
+import DeleteMachineModal from '../../containers/modals/DeleteMachineModal'
+import DeleteRackModal from '../../containers/modals/DeleteRackModal'
+import DeleteRoomModal from '../../containers/modals/DeleteRoomModal'
+import EditRackNameModal from '../../containers/modals/EditRackNameModal'
+import EditRoomNameModal from '../../containers/modals/EditRoomNameModal'
+import NewTopologyModal from '../../containers/modals/NewTopologyModal'
+import AppNavbarContainer from '../../containers/navigation/AppNavbarContainer'
+import ProjectSidebarContainer from '../../containers/app/sidebars/project/ProjectSidebarContainer'
+import { openScenarioSucceeded } from '../../actions/scenarios'
+import NewPortfolioModal from '../../containers/modals/NewPortfolioModal'
+import NewScenarioModal from '../../containers/modals/NewScenarioModal'
+import PortfolioResultsContainer from '../../containers/app/results/PortfolioResultsContainer'
+import KeymapConfiguration from '../../shortcuts/keymap'
+import { useRequireAuth } from '../../auth/hook'
+
+const App = ({ projectId, portfolioId, scenarioId }) => {
+ useRequireAuth()
+
+ const projectName = useSelector(
+ (state) =>
+ state.currentProjectId !== '-1' &&
+ state.objects.project[state.currentProjectId] &&
+ state.objects.project[state.currentProjectId].name
+ )
+ const topologyIsLoading = useSelector((state) => state.currentTopologyIdd === '-1')
+
+ const dispatch = useDispatch()
+ useEffect(() => {
+ if (scenarioId) {
+ dispatch(openScenarioSucceeded(projectId, portfolioId, scenarioId))
+ } else if (portfolioId) {
+ dispatch(openPortfolioSucceeded(projectId, portfolioId))
+ } else {
+ dispatch(openProjectSucceeded(projectId))
+ }
+ }, [projectId, portfolioId, scenarioId, dispatch])
+
+ const constructionElements = topologyIsLoading ? (
+ <div className="full-height d-flex align-items-center justify-content-center">
+ <LoadingScreen />
+ </div>
+ ) : (
+ <div className="full-height">
+ <MapStage />
+ <ScaleIndicatorContainer />
+ <ToolPanelComponent />
+ <ProjectSidebarContainer />
+ <TopologySidebarContainer />
+ </div>
+ )
+
+ const portfolioElements = (
+ <div className="full-height app-page-container">
+ <ProjectSidebarContainer />
+ <div className="container-fluid full-height">
+ <PortfolioResultsContainer />
+ </div>
+ </div>
+ )
+
+ const scenarioElements = (
+ <div className="full-height app-page-container">
+ <ProjectSidebarContainer />
+ <div className="container-fluid full-height">
+ <h2>Scenario loading</h2>
+ </div>
+ </div>
+ )
+
+ const title = projectName ? projectName + ' - OpenDC' : 'Simulation - OpenDC'
+
+ return (
+ <HotKeys keyMap={KeymapConfiguration} allowChanges={true} className="page-container full-height">
+ <Head>
+ <title>{title}</title>
+ </Head>
+ <AppNavbarContainer fullWidth={true} />
+ {scenarioId ? scenarioElements : portfolioId ? portfolioElements : constructionElements}
+ <NewTopologyModal />
+ <NewPortfolioModal />
+ <NewScenarioModal />
+ <EditRoomNameModal />
+ <DeleteRoomModal />
+ <EditRackNameModal />
+ <DeleteRackModal />
+ <DeleteMachineModal />
+ </HotKeys>
+ )
+}
+
+App.propTypes = {
+ projectId: PropTypes.string.isRequired,
+ portfolioId: PropTypes.string,
+ scenarioId: PropTypes.string,
+}
+
+export default App
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js b/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js
index 61d123e8..9394238d 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js
@@ -4,11 +4,13 @@ import { setMapDimensions, setMapPositionWithBoundsCheck, zoomInOnPosition } fro
import MapStageComponent from '../../../components/app/map/MapStageComponent'
const MapStage = () => {
- const { position, dimensions } = useSelector((state) => state.map)
+ const position = useSelector((state) => state.map.position)
+ const dimensions = useSelector((state) => state.map.dimensions)
const dispatch = useDispatch()
const zoomInOnPositionA = (zoomIn, x, y) => dispatch(zoomInOnPosition(zoomIn, x, y))
const setMapPositionWithBoundsCheckA = (x, y) => dispatch(setMapPositionWithBoundsCheck(x, y))
const setMapDimensionsA = (width, height) => dispatch(setMapDimensions(width, height))
+
return (
<MapStageComponent
mapPosition={position}
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js
index 86f465b6..ce295f03 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js
@@ -1,6 +1,6 @@
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
-import { useHistory } from 'react-router-dom'
+import { useRouter } from 'next/router'
import PortfolioListComponent from '../../../../components/app/sidebars/project/PortfolioListComponent'
import { deletePortfolio, setCurrentPortfolio } from '../../../../actions/portfolios'
import { openNewPortfolioModal } from '../../../../actions/modals/portfolios'
@@ -24,7 +24,7 @@ const PortfolioListContainer = (props) => {
})
const dispatch = useDispatch()
- const history = useHistory()
+ const router = useRouter()
const actions = {
onNewPortfolio: () => {
dispatch(openNewPortfolioModal())
@@ -37,7 +37,7 @@ const PortfolioListContainer = (props) => {
const state = await getState(dispatch)
dispatch(deletePortfolio(id))
dispatch(setCurrentTopology(state.objects.project[state.currentProjectId].topologyIds[0]))
- history.push(`/projects/${state.currentProjectId}`)
+ router.push(`/projects/${state.currentProjectId}`)
}
},
}
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js
index 35e6c52b..06c7f0f7 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js
@@ -1,11 +1,11 @@
import React from 'react'
-import { useLocation } from 'react-router-dom'
+import { useRouter } from 'next/router'
import ProjectSidebarComponent from '../../../../components/app/sidebars/project/ProjectSidebarComponent'
import { isCollapsible } from '../../../../util/sidebar-space'
const ProjectSidebarContainer = (props) => {
- const location = useLocation()
- return <ProjectSidebarComponent collapsible={isCollapsible(location)} {...props} />
+ const router = useRouter()
+ return <ProjectSidebarComponent collapsible={isCollapsible(router)} {...props} />
}
export default ProjectSidebarContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js
index 954284a6..e9c05f17 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js
@@ -3,13 +3,13 @@ import { useDispatch, useSelector } from 'react-redux'
import TopologyListComponent from '../../../../components/app/sidebars/project/TopologyListComponent'
import { setCurrentTopology } from '../../../../actions/topology/building'
import { openNewTopologyModal } from '../../../../actions/modals/topology'
-import { useHistory } from 'react-router-dom'
+import { useRouter } from 'next/router'
import { getState } from '../../../../util/state-utils'
import { deleteTopology } from '../../../../actions/topologies'
const TopologyListContainer = () => {
const dispatch = useDispatch()
- const history = useHistory()
+ const router = useRouter()
const topologies = useSelector((state) => {
let topologies = state.objects.project[state.currentProjectId]
@@ -26,7 +26,7 @@ const TopologyListContainer = () => {
const onChooseTopology = async (id) => {
dispatch(setCurrentTopology(id))
const state = await getState(dispatch)
- history.push(`/projects/${state.currentProjectId}`)
+ router.push(`/projects/${state.currentProjectId}`)
}
const onNewTopology = () => {
dispatch(openNewTopologyModal())
@@ -36,7 +36,7 @@ const TopologyListContainer = () => {
const state = await getState(dispatch)
dispatch(deleteTopology(id))
dispatch(setCurrentTopology(state.objects.project[state.currentProjectId].topologyIds[0]))
- history.push(`/projects/${state.currentProjectId}`)
+ router.push(`/projects/${state.currentProjectId}`)
}
}
diff --git a/opendc-web/opendc-web-ui/src/containers/auth/Login.js b/opendc-web/opendc-web-ui/src/containers/auth/Login.js
index f652429d..178f269e 100644
--- a/opendc-web/opendc-web-ui/src/containers/auth/Login.js
+++ b/opendc-web/opendc-web-ui/src/containers/auth/Login.js
@@ -3,7 +3,6 @@ import GoogleLogin from 'react-google-login'
import { useDispatch } from 'react-redux'
import { logIn } from '../../actions/auth'
import { Button } from 'reactstrap'
-import config from '../../config'
function Login({ visible, className }) {
const dispatch = useDispatch()
@@ -30,12 +29,12 @@ function Login({ visible, className }) {
return (
<GoogleLogin
- clientId={config.OAUTH_CLIENT_ID}
+ clientId={process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID}
onSuccess={onAuthResponse}
onFailure={onAuthFailure}
render={(renderProps) => (
<Button color="primary" onClick={renderProps.onClick} className={className}>
- <span className="fa fa-google" /> Login with Google
+ <span aria-hidden className="fa fa-google" /> Login with Google
</Button>
)}
/>
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js b/opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js
deleted file mode 100644
index 93a38642..00000000
--- a/opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import { closeDeleteProfileModal } from '../../actions/modals/profile'
-import { deleteCurrentUser } from '../../actions/users'
-import ConfirmationModal from '../../components/modals/ConfirmationModal'
-
-const DeleteProfileModal = () => {
- const visible = useSelector((state) => state.modals.deleteProfileModalVisible)
-
- const dispatch = useDispatch()
- const callback = (isConfirmed) => {
- if (isConfirmed) {
- dispatch(deleteCurrentUser())
- }
- dispatch(closeDeleteProfileModal())
- }
- return (
- <ConfirmationModal
- title="Delete my account"
- message="Are you sure you want to delete your OpenDC account?"
- show={visible}
- callback={callback}
- />
- )
-}
-
-export default DeleteProfileModal
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js b/opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js
index b588b4bc..18ad65f9 100644
--- a/opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js
+++ b/opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js
@@ -6,8 +6,6 @@ import { closeNewScenarioModal } from '../../actions/modals/scenarios'
const NewScenarioModal = (props) => {
const topologies = useSelector(({ currentProjectId, objects }) => {
- console.log(currentProjectId, objects)
-
if (currentProjectId === '-1' || !objects.project[currentProjectId]) {
return []
}
diff --git a/opendc-web/opendc-web-ui/src/index.js b/opendc-web/opendc-web-ui/src/index.js
deleted file mode 100644
index fdfec24b..00000000
--- a/opendc-web/opendc-web-ui/src/index.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react'
-import ReactDOM from 'react-dom'
-import * as Sentry from '@sentry/react'
-import { Integrations } from '@sentry/tracing'
-import { Provider } from 'react-redux'
-import './index.scss'
-import Routes from './routes'
-import config from './config'
-import configureStore from './store/configure-store'
-
-const store = configureStore()
-
-// Initialize Sentry if the user has configured a DSN
-const dsn = config['SENTRY_DSN']
-if (dsn) {
- Sentry.init({
- environment: process.env.NODE_ENV,
- dsn: dsn,
- integrations: [new Integrations.BrowserTracing()],
- tracesSampleRate: 0.1,
- })
-}
-
-ReactDOM.render(
- <Provider store={store}>
- <Routes />
- </Provider>,
- document.getElementById('root')
-)
diff --git a/opendc-web/opendc-web-ui/src/index.scss b/opendc-web/opendc-web-ui/src/index.scss
index 0c1ddff4..1a4d1303 100644
--- a/opendc-web/opendc-web-ui/src/index.scss
+++ b/opendc-web/opendc-web-ui/src/index.scss
@@ -5,7 +5,7 @@
html,
body,
-#root {
+#__next {
margin: 0;
padding: 0;
width: 100%;
diff --git a/opendc-web/opendc-web-ui/src/pages/404.js b/opendc-web/opendc-web-ui/src/pages/404.js
new file mode 100644
index 00000000..cc9281fc
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/404.js
@@ -0,0 +1,19 @@
+import React from 'react'
+import Head from 'next/head'
+import TerminalWindow from '../components/not-found/TerminalWindow'
+import style from './404.module.scss'
+
+const NotFound = () => {
+ return (
+ <>
+ <Head>
+ <title>Page Not Found - OpenDC</title>
+ </Head>
+ <div className={style['not-found-backdrop']}>
+ <TerminalWindow />
+ </div>
+ </>
+ )
+}
+
+export default NotFound
diff --git a/opendc-web/opendc-web-ui/src/pages/NotFound.module.scss b/opendc-web/opendc-web-ui/src/pages/404.module.scss
index e91c2780..e91c2780 100644
--- a/opendc-web/opendc-web-ui/src/pages/NotFound.module.scss
+++ b/opendc-web/opendc-web-ui/src/pages/404.module.scss
diff --git a/opendc-web/opendc-web-ui/src/pages/App.js b/opendc-web/opendc-web-ui/src/pages/App.js
deleted file mode 100644
index ea62e8dc..00000000
--- a/opendc-web/opendc-web-ui/src/pages/App.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import PropTypes from 'prop-types'
-import React, { useEffect } from 'react'
-import { HotKeys } from 'react-hotkeys'
-import { useDispatch, useSelector } from 'react-redux'
-import { openPortfolioSucceeded } from '../actions/portfolios'
-import { openProjectSucceeded } from '../actions/projects'
-import ToolPanelComponent from '../components/app/map/controls/ToolPanelComponent'
-import LoadingScreen from '../components/app/map/LoadingScreen'
-import ScaleIndicatorContainer from '../containers/app/map/controls/ScaleIndicatorContainer'
-import MapStage from '../containers/app/map/MapStage'
-import TopologySidebarContainer from '../containers/app/sidebars/topology/TopologySidebarContainer'
-import DeleteMachineModal from '../containers/modals/DeleteMachineModal'
-import DeleteRackModal from '../containers/modals/DeleteRackModal'
-import DeleteRoomModal from '../containers/modals/DeleteRoomModal'
-import EditRackNameModal from '../containers/modals/EditRackNameModal'
-import EditRoomNameModal from '../containers/modals/EditRoomNameModal'
-import NewTopologyModal from '../containers/modals/NewTopologyModal'
-import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
-import ProjectSidebarContainer from '../containers/app/sidebars/project/ProjectSidebarContainer'
-import { openScenarioSucceeded } from '../actions/scenarios'
-import NewPortfolioModal from '../containers/modals/NewPortfolioModal'
-import NewScenarioModal from '../containers/modals/NewScenarioModal'
-import PortfolioResultsContainer from '../containers/app/results/PortfolioResultsContainer'
-import KeymapConfiguration from '../shortcuts/keymap'
-import { useDocumentTitle } from '../util/hooks'
-
-const App = ({ projectId, portfolioId, scenarioId }) => {
- const projectName = useSelector(
- (state) =>
- state.currentProjectId !== '-1' &&
- state.objects.project[state.currentProjectId] &&
- state.objects.project[state.currentProjectId].name
- )
- const topologyIsLoading = useSelector((state) => state.currentTopologyId === '-1')
-
- const dispatch = useDispatch()
- useEffect(() => {
- if (scenarioId) {
- dispatch(openScenarioSucceeded(projectId, portfolioId, scenarioId))
- } else if (portfolioId) {
- dispatch(openPortfolioSucceeded(projectId, portfolioId))
- } else {
- dispatch(openProjectSucceeded(projectId))
- }
- }, [projectId, portfolioId, scenarioId, dispatch])
-
- const constructionElements = topologyIsLoading ? (
- <div className="full-height d-flex align-items-center justify-content-center">
- <LoadingScreen />
- </div>
- ) : (
- <div className="full-height">
- <MapStage />
- <ScaleIndicatorContainer />
- <ToolPanelComponent />
- <ProjectSidebarContainer />
- <TopologySidebarContainer />
- </div>
- )
-
- const portfolioElements = (
- <div className="full-height app-page-container">
- <ProjectSidebarContainer />
- <div className="container-fluid full-height">
- <PortfolioResultsContainer />
- </div>
- </div>
- )
-
- const scenarioElements = (
- <div className="full-height app-page-container">
- <ProjectSidebarContainer />
- <div className="container-fluid full-height">
- <h2>Scenario loading</h2>
- </div>
- </div>
- )
-
- useDocumentTitle(projectName ? projectName + ' - OpenDC' : 'Simulation - OpenDC')
-
- return (
- <HotKeys keyMap={KeymapConfiguration} className="page-container full-height">
- <AppNavbarContainer fullWidth={true} />
- {scenarioId ? scenarioElements : portfolioId ? portfolioElements : constructionElements}
- <NewTopologyModal />
- <NewPortfolioModal />
- <NewScenarioModal />
- <EditRoomNameModal />
- <DeleteRoomModal />
- <EditRackNameModal />
- <DeleteRackModal />
- <DeleteMachineModal />
- </HotKeys>
- )
-}
-
-App.propTypes = {
- projectId: PropTypes.string.isRequired,
- portfolioId: PropTypes.string,
- scenarioId: PropTypes.string,
-}
-
-export default App
diff --git a/opendc-web/opendc-web-ui/src/pages/NotFound.js b/opendc-web/opendc-web-ui/src/pages/NotFound.js
deleted file mode 100644
index 409ffa0e..00000000
--- a/opendc-web/opendc-web-ui/src/pages/NotFound.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import TerminalWindow from '../components/not-found/TerminalWindow'
-import style from './NotFound.module.scss'
-import { useDocumentTitle } from '../util/hooks'
-
-const NotFound = () => {
- useDocumentTitle('Page Not Found - OpenDC')
- return (
- <div className={style['not-found-backdrop']}>
- <TerminalWindow />
- </div>
- )
-}
-
-export default NotFound
diff --git a/opendc-web/opendc-web-ui/src/pages/Profile.js b/opendc-web/opendc-web-ui/src/pages/Profile.js
deleted file mode 100644
index ea781686..00000000
--- a/opendc-web/opendc-web-ui/src/pages/Profile.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react'
-import { useDispatch } from 'react-redux'
-import { openDeleteProfileModal } from '../actions/modals/profile'
-import DeleteProfileModal from '../containers/modals/DeleteProfileModal'
-import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
-import { useDocumentTitle } from '../util/hooks'
-
-const Profile = () => {
- const dispatch = useDispatch()
- const onDelete = () => dispatch(openDeleteProfileModal())
-
- useDocumentTitle('My Profile - OpenDC')
- return (
- <div className="full-height">
- <AppNavbarContainer fullWidth={false} />
- <div className="container text-page-container full-height">
- <button className="btn btn-danger mb-2 ml-auto mr-auto" style={{ maxWidth: 300 }} onClick={onDelete}>
- Delete my account on OpenDC
- </button>
- <p className="text-muted text-center">
- This does not delete your Google account, but simply disconnects it from the OpenDC platform and
- deletes any project info that is associated with you (projects you own and any authorizations you
- may have on other projects).
- </p>
- </div>
- <DeleteProfileModal />
- </div>
- )
-}
-
-export default Profile
diff --git a/opendc-web/opendc-web-ui/src/pages/Projects.js b/opendc-web/opendc-web-ui/src/pages/Projects.js
deleted file mode 100644
index 5e642a03..00000000
--- a/opendc-web/opendc-web-ui/src/pages/Projects.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React, { useEffect } from 'react'
-import { useDispatch } from 'react-redux'
-import { fetchAuthorizationsOfCurrentUser } from '../actions/users'
-import ProjectFilterPanel from '../components/projects/FilterPanel'
-import NewProjectModal from '../containers/modals/NewProjectModal'
-import NewProjectButtonContainer from '../containers/projects/NewProjectButtonContainer'
-import VisibleProjectList from '../containers/projects/VisibleProjectAuthList'
-import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
-import { useDocumentTitle } from '../util/hooks'
-
-function Projects() {
- const dispatch = useDispatch()
-
- useEffect(() => dispatch(fetchAuthorizationsOfCurrentUser()))
- useDocumentTitle('My Projects - OpenDC')
-
- return (
- <div className="full-height">
- <AppNavbarContainer fullWidth={false} />
- <div className="container text-page-container full-height">
- <ProjectFilterPanel />
- <VisibleProjectList />
- <NewProjectButtonContainer />
- </div>
- <NewProjectModal />
- </div>
- )
-}
-
-export default Projects
diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js
new file mode 100644
index 00000000..77ffa698
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/_app.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import Head from 'next/head'
+import { Provider } from 'react-redux'
+import { useStore } from '../store/configure-store'
+import '../index.scss'
+
+export default function App({ Component, pageProps }) {
+ const store = useStore(pageProps.initialReduxState)
+
+ return (
+ <>
+ <Head>
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
+ <meta name="theme-color" content="#00A6D6" />
+ </Head>
+ <Provider store={store}>
+ <Component {...pageProps} />
+ </Provider>
+ </>
+ )
+}
diff --git a/opendc-web/opendc-web-ui/src/pages/_document.js b/opendc-web/opendc-web-ui/src/pages/_document.js
new file mode 100644
index 00000000..943ae395
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/_document.js
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import Document, { Html, Head, Main, NextScript } from 'next/document'
+
+class OpenDCDocument extends Document {
+ render() {
+ return (
+ <Html>
+ <Head>
+ <meta charSet="utf-8" />
+ <meta name="theme-color" content="#00A6D6" />
+ <meta
+ name="description"
+ content="Collaborative Datacenter Simulation and Exploration for Everybody"
+ />
+ <meta name="author" content="@Large Research" />
+ <meta
+ name="keywords"
+ content="OpenDC, Datacenter, Simulation, Simulator, Collaborative, Distributed, Cluster"
+ />
+ <link rel="manifest" href="/manifest.json" />
+ <link rel="shortcut icon" href="/favicon.ico" />
+
+ {/* Twitter Card data */}
+ <meta name="twitter:card" content="summary" />
+ <meta name="twitter:site" content="@LargeResearch" />
+ <meta name="twitter:title" content="OpenDC" />
+ <meta
+ name="twitter:description"
+ content="Collaborative Datacenter Simulation and Exploration for Everybody"
+ />
+ <meta name="twitter:creator" content="@LargeResearch" />
+ <meta name="twitter:image" content="http://opendc.org/img/logo.png" />
+
+ {/* OpenGraph meta tags */}
+ <meta property="og:title" content="OpenDC" />
+ <meta property="og:site_name" content="OpenDC" />
+ <meta property="og:type" content="website" />
+ <meta property="og:image" content="http://opendc.org/img/logo.png" />
+ <meta property="og:url" content="http://opendc.org/" />
+ <meta
+ property="og:description"
+ content="OpenDC provides collaborative online datacenter modeling, diverse and effective datacenter simulation, and exploratory datacenter performance feedback."
+ />
+ <meta property="og:locale" content="en_US" />
+
+ {/* CDN Dependencies */}
+ <link
+ href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
+ rel="stylesheet"
+ />
+ <script async src="https://use.fontawesome.com/ece66a2e7c.js" />
+
+ {/* Google Analytics */}
+ <script async src="https://www.googletagmanager.com/gtag/js?id=UA-84285092-3" />
+ <script
+ dangerouslySetInnerHTML={{
+ __html: `
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'UA-84285092-3');
+ `,
+ }}
+ />
+ </Head>
+ <body>
+ <Main />
+ <NextScript />
+ </body>
+ </Html>
+ )
+ }
+}
+
+export default OpenDCDocument
diff --git a/opendc-web/opendc-web-ui/src/pages/Home.js b/opendc-web/opendc-web-ui/src/pages/index.js
index ee930fbe..bb904eb6 100644
--- a/opendc-web/opendc-web-ui/src/pages/Home.js
+++ b/opendc-web/opendc-web-ui/src/pages/index.js
@@ -1,4 +1,5 @@
import React from 'react'
+import Head from 'next/head'
import ContactSection from '../components/home/ContactSection'
import IntroSection from '../components/home/IntroSection'
import JumbotronHeader from '../components/home/JumbotronHeader'
@@ -15,13 +16,14 @@ import {
simulationSection,
technologiesSection,
teamSection,
-} from './Home.module.scss'
-import { useDocumentTitle } from '../util/hooks'
+} from './index.module.scss'
function Home() {
- useDocumentTitle('OpenDC')
return (
- <div>
+ <>
+ <Head>
+ <title>OpenDC</title>
+ </Head>
<HomeNavbar />
<div className="body-wrapper page-container">
<JumbotronHeader />
@@ -33,7 +35,7 @@ function Home() {
<TeamSection className={teamSection} />
<ContactSection />
</div>
- </div>
+ </>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/Home.module.scss b/opendc-web/opendc-web-ui/src/pages/index.module.scss
index aed1d88f..aed1d88f 100644
--- a/opendc-web/opendc-web-ui/src/pages/Home.module.scss
+++ b/opendc-web/opendc-web-ui/src/pages/index.module.scss
diff --git a/opendc-web/opendc-web-ui/src/pages/profile.js b/opendc-web/opendc-web-ui/src/pages/profile.js
new file mode 100644
index 00000000..de60037b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/profile.js
@@ -0,0 +1,54 @@
+import React, { useState } from 'react'
+import Head from 'next/head'
+import { useDispatch } from 'react-redux'
+import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
+import ConfirmationModal from '../components/modals/ConfirmationModal'
+import { deleteCurrentUser } from '../actions/users'
+import { useRequireAuth } from '../auth/hook'
+
+const Profile = () => {
+ useRequireAuth()
+
+ const dispatch = useDispatch()
+
+ const [isDeleteModalOpen, setDeleteModalOpen] = useState(false)
+ const onDelete = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteCurrentUser())
+ }
+ setDeleteModalOpen(false)
+ }
+
+ return (
+ <>
+ <Head>
+ <title>My Profile - OpenDC</title>
+ </Head>
+ <div className="full-height">
+ <AppNavbarContainer fullWidth={false} />
+ <div className="container text-page-container full-height">
+ <button
+ className="btn btn-danger mb-2 ml-auto mr-auto"
+ style={{ maxWidth: 300 }}
+ onClick={() => setDeleteModalOpen(true)}
+ >
+ Delete my account on OpenDC
+ </button>
+ <p className="text-muted text-center">
+ This does not delete your Google account, but simply disconnects it from the OpenDC platform and
+ deletes any project info that is associated with you (projects you own and any authorizations
+ you may have on other projects).
+ </p>
+ </div>
+ <ConfirmationModal
+ title="Delete my account"
+ message="Are you sure you want to delete your OpenDC account?"
+ show={isDeleteModalOpen}
+ callback={onDelete}
+ />
+ </div>
+ </>
+ )
+}
+
+export default Profile
diff --git a/opendc-web/opendc-web-ui/src/util/hooks.js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
index 7780a778..72316bc9 100644
--- a/opendc-web/opendc-web-ui/src/util/hooks.js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
@@ -20,10 +20,18 @@
* SOFTWARE.
*/
-import { useEffect } from 'react'
+import { useRouter } from 'next/router'
+import App from '../../../containers/app/App'
-export function useDocumentTitle(title) {
- useEffect(() => {
- document.title = title
- }, [title])
+function Project() {
+ const router = useRouter()
+ const { project } = router.query
+
+ if (project) {
+ return <App projectId={project} />
+ }
+
+ return <div />
}
+
+export default Project
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
new file mode 100644
index 00000000..76a8d23b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useRouter } from 'next/router'
+import App from '../../../../containers/app/App'
+
+function Project() {
+ const router = useRouter()
+ const { project, portfolio } = router.query
+
+ if (project && portfolio) {
+ return <App projectId={project} portfolioId={portfolio} />
+ }
+
+ return <div />
+}
+
+export default Project
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/index.js b/opendc-web/opendc-web-ui/src/pages/projects/index.js
new file mode 100644
index 00000000..bea9ad93
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js
@@ -0,0 +1,36 @@
+import React, { useEffect } from 'react'
+import Head from 'next/head'
+import { useDispatch } from 'react-redux'
+import { fetchAuthorizationsOfCurrentUser } from '../../actions/users'
+import ProjectFilterPanel from '../../components/projects/FilterPanel'
+import NewProjectModal from '../../containers/modals/NewProjectModal'
+import NewProjectButtonContainer from '../../containers/projects/NewProjectButtonContainer'
+import VisibleProjectList from '../../containers/projects/VisibleProjectAuthList'
+import AppNavbarContainer from '../../containers/navigation/AppNavbarContainer'
+import { useRequireAuth } from '../../auth/hook'
+
+function Projects() {
+ const dispatch = useDispatch()
+
+ useRequireAuth()
+ useEffect(() => dispatch(fetchAuthorizationsOfCurrentUser()))
+
+ return (
+ <>
+ <Head>
+ <title>My Projects - OpenDC</title>
+ </Head>
+ <div className="full-height">
+ <AppNavbarContainer fullWidth={false} />
+ <div className="container text-page-container full-height">
+ <ProjectFilterPanel />
+ <VisibleProjectList />
+ <NewProjectButtonContainer />
+ </div>
+ <NewProjectModal />
+ </div>
+ </>
+ )
+}
+
+export default Projects
diff --git a/opendc-web/opendc-web-ui/src/reducers/modals.js b/opendc-web/opendc-web-ui/src/reducers/modals.js
index a7656373..e600d556 100644
--- a/opendc-web/opendc-web-ui/src/reducers/modals.js
+++ b/opendc-web/opendc-web-ui/src/reducers/modals.js
@@ -1,5 +1,4 @@
import { combineReducers } from 'redux'
-import { CLOSE_DELETE_PROFILE_MODAL, OPEN_DELETE_PROFILE_MODAL } from '../actions/modals/profile'
import { CLOSE_NEW_PROJECT_MODAL, OPEN_NEW_PROJECT_MODAL } from '../actions/modals/projects'
import {
CLOSE_NEW_TOPOLOGY_MODAL,
@@ -33,7 +32,6 @@ function modal(openAction, closeAction) {
export const modals = combineReducers({
newProjectModalVisible: modal(OPEN_NEW_PROJECT_MODAL, CLOSE_NEW_PROJECT_MODAL),
- deleteProfileModalVisible: modal(OPEN_DELETE_PROFILE_MODAL, CLOSE_DELETE_PROFILE_MODAL),
changeTopologyModalVisible: modal(OPEN_NEW_TOPOLOGY_MODAL, CLOSE_NEW_TOPOLOGY_MODAL),
editRoomNameModalVisible: modal(OPEN_EDIT_ROOM_NAME_MODAL, CLOSE_EDIT_ROOM_NAME_MODAL),
deleteRoomModalVisible: modal(OPEN_DELETE_ROOM_MODAL, CLOSE_DELETE_ROOM_MODAL),
diff --git a/opendc-web/opendc-web-ui/src/routes/index.js b/opendc-web/opendc-web-ui/src/routes/index.js
deleted file mode 100644
index 4291a046..00000000
--- a/opendc-web/opendc-web-ui/src/routes/index.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react'
-import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
-import { userIsLoggedIn } from '../auth/index'
-import App from '../pages/App'
-import Home from '../pages/Home'
-import NotFound from '../pages/NotFound'
-import Profile from '../pages/Profile'
-import Projects from '../pages/Projects'
-
-const ProtectedComponent = (component) => () => (userIsLoggedIn() ? component : <Redirect to="/" />)
-const AppComponent = ({ match }) =>
- userIsLoggedIn() ? (
- <App
- projectId={match.params.projectId}
- portfolioId={match.params.portfolioId}
- scenarioId={match.params.scenarioId}
- />
- ) : (
- <Redirect to="/" />
- )
-
-const Routes = () => (
- <BrowserRouter>
- <Switch>
- <Route exact path="/" component={Home} />
- <Route exact path="/projects" render={ProtectedComponent(<Projects />)} />
- <Route exact path="/projects/:projectId" component={AppComponent} />
- <Route exact path="/projects/:projectId/portfolios/:portfolioId" component={AppComponent} />
- <Route
- exact
- path="/projects/:projectId/portfolios/:portfolioId/scenarios/:scenarioId"
- component={AppComponent}
- />
- <Route exact path="/profile" render={ProtectedComponent(<Profile />)} />
- <Route path="/*" component={NotFound} />
- </Switch>
- </BrowserRouter>
-)
-
-export default Routes
diff --git a/opendc-web/opendc-web-ui/src/store/configure-store.js b/opendc-web/opendc-web-ui/src/store/configure-store.js
index 13bcd69e..149536a3 100644
--- a/opendc-web/opendc-web-ui/src/store/configure-store.js
+++ b/opendc-web/opendc-web-ui/src/store/configure-store.js
@@ -1,6 +1,7 @@
+import { useMemo } from 'react'
import { applyMiddleware, compose, createStore } from 'redux'
-import persistState from 'redux-localstorage'
import { createLogger } from 'redux-logger'
+import persistState from 'redux-localstorage'
import createSagaMiddleware from 'redux-saga'
import thunk from 'redux-thunk'
import { authRedirectMiddleware } from '../auth/index'
@@ -8,20 +9,52 @@ import rootReducer from '../reducers/index'
import rootSaga from '../sagas/index'
import { viewportAdjustmentMiddleware } from './middlewares/viewport-adjustment'
-const sagaMiddleware = createSagaMiddleware()
+let store
-const middlewares = [thunk, sagaMiddleware, authRedirectMiddleware, viewportAdjustmentMiddleware]
+function initStore(initialState) {
+ const sagaMiddleware = createSagaMiddleware()
-if (process.env.NODE_ENV !== 'production') {
- middlewares.push(createLogger())
-}
+ const middlewares = [thunk, sagaMiddleware, authRedirectMiddleware, viewportAdjustmentMiddleware]
-export let store = undefined
+ if (process.env.NODE_ENV !== 'production') {
+ middlewares.push(createLogger())
+ }
-export default function configureStore() {
- const configuredStore = createStore(rootReducer, compose(persistState('auth'), applyMiddleware(...middlewares)))
+ let enhancer = applyMiddleware(...middlewares)
+
+ if (global.localStorage) {
+ enhancer = compose(persistState('auth'), enhancer)
+ }
+
+ const configuredStore = createStore(rootReducer, enhancer)
sagaMiddleware.run(rootSaga)
store = configuredStore
return configuredStore
}
+
+export const initializeStore = (preloadedState) => {
+ let _store = store ?? initStore(preloadedState)
+
+ // After navigating to a page with an initial Redux state, merge that state
+ // with the current state in the store, and create a new store
+ if (preloadedState && store) {
+ _store = initStore({
+ ...store.getState(),
+ ...preloadedState,
+ })
+ // Reset the current store
+ store = undefined
+ }
+
+ // For SSG and SSR always create a new store
+ if (typeof window === 'undefined') return _store
+ // Create the store once in the client
+ if (!store) store = _store
+
+ return _store
+}
+
+export function useStore(initialState) {
+ return useMemo(() => initializeStore(initialState), [initialState])
+}
diff --git a/opendc-web/opendc-web-ui/src/util/sidebar-space.js b/opendc-web/opendc-web-ui/src/util/sidebar-space.js
index ef09d40a..005c40f3 100644
--- a/opendc-web/opendc-web-ui/src/util/sidebar-space.js
+++ b/opendc-web/opendc-web-ui/src/util/sidebar-space.js
@@ -1,2 +1,2 @@
-export const isCollapsible = (location) =>
- location.pathname.indexOf('portfolios') === -1 && location.pathname.indexOf('scenarios') === -1
+export const isCollapsible = (router) =>
+ router.asPath.indexOf('portfolios') === -1 && router.asPath.indexOf('scenarios') === -1