summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src
diff options
context:
space:
mode:
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