summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web/opendc-web-ui/src/pages')
-rw-r--r--opendc-web/opendc-web-ui/src/pages/404.js38
-rw-r--r--opendc-web/opendc-web-ui/src/pages/App.js137
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Home.js33
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Home.sass9
-rw-r--r--opendc-web/opendc-web-ui/src/pages/NotFound.js14
-rw-r--r--opendc-web/opendc-web-ui/src/pages/NotFound.sass11
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Profile.js35
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Projects.js43
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_app.js96
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_document.js95
-rw-r--r--opendc-web/opendc-web-ui/src/pages/logout.js39
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js83
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js117
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js140
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/index.js87
15 files changed, 695 insertions, 282 deletions
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..0939bc56
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/404.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import Head from 'next/head'
+import { AppPage } from '../components/AppPage'
+import {
+ Bullseye,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ PageSection,
+ PageSectionVariants,
+ Title,
+} from '@patternfly/react-core'
+import { UnknownIcon } from '@patternfly/react-icons'
+
+const NotFound = () => {
+ return (
+ <AppPage>
+ <Head>
+ <title>Page Not Found - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={UnknownIcon} />
+ <Title size="lg" headingLevel="h4">
+ 404: That page does not exist
+ </Title>
+ <EmptyStateBody>
+ The requested page is not found. Try refreshing the page if it was recently added.
+ </EmptyStateBody>
+ </EmptyState>
+ </Bullseye>
+ </PageSection>
+ </AppPage>
+ )
+}
+
+export default NotFound
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 cbc805b8..00000000
--- a/opendc-web/opendc-web-ui/src/pages/App.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import DocumentTitle from 'react-document-title'
-import { connect } from 'react-redux'
-import { ShortcutManager } from 'react-shortcuts'
-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 KeymapConfiguration from '../shortcuts/keymap'
-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'
-
-const shortcutManager = new ShortcutManager(KeymapConfiguration)
-
-class AppComponent extends React.Component {
- static propTypes = {
- projectId: PropTypes.string.isRequired,
- portfolioId: PropTypes.string,
- scenarioId: PropTypes.string,
- projectName: PropTypes.string,
- }
- static childContextTypes = {
- shortcuts: PropTypes.object.isRequired,
- }
-
- componentDidMount() {
- if (this.props.scenarioId) {
- this.props.openScenarioSucceeded(this.props.projectId, this.props.portfolioId, this.props.scenarioId)
- } else if (this.props.portfolioId) {
- this.props.openPortfolioSucceeded(this.props.projectId, this.props.portfolioId)
- } else {
- this.props.openProjectSucceeded(this.props.projectId)
- }
- }
-
- getChildContext() {
- return {
- shortcuts: shortcutManager,
- }
- }
-
- render() {
- const constructionElements = this.props.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>
- )
-
- return (
- <DocumentTitle
- title={this.props.projectName ? this.props.projectName + ' - OpenDC' : 'Simulation - OpenDC'}
- >
- <div className="page-container full-height">
- <AppNavbarContainer fullWidth={true} />
- {this.props.scenarioId
- ? scenarioElements
- : this.props.portfolioId
- ? portfolioElements
- : constructionElements}
- <NewTopologyModal />
- <NewPortfolioModal />
- <NewScenarioModal />
- <EditRoomNameModal />
- <DeleteRoomModal />
- <EditRackNameModal />
- <DeleteRackModal />
- <DeleteMachineModal />
- </div>
- </DocumentTitle>
- )
- }
-}
-
-const mapStateToProps = (state) => {
- let projectName = undefined
- if (state.currentProjectId !== '-1' && state.objects.project[state.currentProjectId]) {
- projectName = state.objects.project[state.currentProjectId].name
- }
-
- return {
- topologyIsLoading: state.currentTopologyId === '-1',
- projectName,
- }
-}
-
-const mapDispatchToProps = (dispatch) => {
- return {
- openProjectSucceeded: (projectId) => dispatch(openProjectSucceeded(projectId)),
- openPortfolioSucceeded: (projectId, portfolioId) => dispatch(openPortfolioSucceeded(projectId, portfolioId)),
- openScenarioSucceeded: (projectId, portfolioId, scenarioId) =>
- dispatch(openScenarioSucceeded(projectId, portfolioId, scenarioId)),
- }
-}
-
-const App = connect(mapStateToProps, mapDispatchToProps)(AppComponent)
-
-export default App
diff --git a/opendc-web/opendc-web-ui/src/pages/Home.js b/opendc-web/opendc-web-ui/src/pages/Home.js
deleted file mode 100644
index 6fc940c0..00000000
--- a/opendc-web/opendc-web-ui/src/pages/Home.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react'
-import DocumentTitle from 'react-document-title'
-import ContactSection from '../components/home/ContactSection'
-import IntroSection from '../components/home/IntroSection'
-import JumbotronHeader from '../components/home/JumbotronHeader'
-import ModelingSection from '../components/home/ModelingSection'
-import SimulationSection from '../components/home/SimulationSection'
-import StakeholderSection from '../components/home/StakeholderSection'
-import TeamSection from '../components/home/TeamSection'
-import TechnologiesSection from '../components/home/TechnologiesSection'
-import HomeNavbar from '../components/navigation/HomeNavbar'
-import './Home.sass'
-
-function Home() {
- return (
- <div>
- <HomeNavbar />
- <div className="body-wrapper page-container">
- <JumbotronHeader />
- <IntroSection />
- <StakeholderSection />
- <ModelingSection />
- <SimulationSection />
- <TechnologiesSection />
- <TeamSection />
- <ContactSection />
- <DocumentTitle title="OpenDC" />
- </div>
- </div>
- )
-}
-
-export default Home
diff --git a/opendc-web/opendc-web-ui/src/pages/Home.sass b/opendc-web/opendc-web-ui/src/pages/Home.sass
deleted file mode 100644
index 79cb9698..00000000
--- a/opendc-web/opendc-web-ui/src/pages/Home.sass
+++ /dev/null
@@ -1,9 +0,0 @@
-.body-wrapper
- position: relative
- overflow-y: hidden
-
-.intro-section, .modeling-section, .technologies-section
- background-color: #fff
-
-.stakeholder-section, .simulation-section, .team-section
- background-color: #f2f2f2
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 72be7342..00000000
--- a/opendc-web/opendc-web-ui/src/pages/NotFound.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react'
-import DocumentTitle from 'react-document-title'
-import TerminalWindow from '../components/not-found/TerminalWindow'
-import './NotFound.sass'
-
-const NotFound = () => (
- <DocumentTitle title="Page Not Found - OpenDC">
- <div className="not-found-backdrop">
- <TerminalWindow />
- </div>
- </DocumentTitle>
-)
-
-export default NotFound
diff --git a/opendc-web/opendc-web-ui/src/pages/NotFound.sass b/opendc-web/opendc-web-ui/src/pages/NotFound.sass
deleted file mode 100644
index 59231f7a..00000000
--- a/opendc-web/opendc-web-ui/src/pages/NotFound.sass
+++ /dev/null
@@ -1,11 +0,0 @@
-.not-found-backdrop
- position: absolute
- left: 0
- top: 0
-
- margin: 0
- padding: 0
- width: 100%
- height: 100%
-
- background-image: linear-gradient(135deg, #00678a, #008fbf, #00A6D6)
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 0d94b519..00000000
--- a/opendc-web/opendc-web-ui/src/pages/Profile.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react'
-import DocumentTitle from 'react-document-title'
-import { connect } from 'react-redux'
-import { openDeleteProfileModal } from '../actions/modals/profile'
-import DeleteProfileModal from '../containers/modals/DeleteProfileModal'
-import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
-
-const ProfileContainer = ({ onDelete }) => (
- <DocumentTitle title="My Profile - OpenDC">
- <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>
- </DocumentTitle>
-)
-
-const mapDispatchToProps = (dispatch) => {
- return {
- onDelete: () => dispatch(openDeleteProfileModal()),
- }
-}
-
-const Profile = connect(undefined, mapDispatchToProps)(ProfileContainer)
-
-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 bb54aaa5..00000000
--- a/opendc-web/opendc-web-ui/src/pages/Projects.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react'
-import DocumentTitle from 'react-document-title'
-import { connect } from 'react-redux'
-import { openNewProjectModal } from '../actions/modals/projects'
-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'
-
-class ProjectsContainer extends React.Component {
- componentDidMount() {
- this.props.fetchAuthorizationsOfCurrentUser()
- }
-
- render() {
- return (
- <DocumentTitle title="My Projects - OpenDC">
- <div className="full-height">
- <AppNavbarContainer fullWidth={false} />
- <div className="container text-page-container full-height">
- <ProjectFilterPanel />
- <VisibleProjectList />
- <NewProjectButtonContainer />
- </div>
- <NewProjectModal />
- </div>
- </DocumentTitle>
- )
- }
-}
-
-const mapDispatchToProps = (dispatch) => {
- return {
- fetchAuthorizationsOfCurrentUser: () => dispatch(fetchAuthorizationsOfCurrentUser()),
- openNewProjectModal: () => dispatch(openNewProjectModal()),
- }
-}
-
-const Projects = connect(undefined, mapDispatchToProps)(ProjectsContainer)
-
-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..900ff405
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/_app.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 PropTypes from 'prop-types'
+import Head from 'next/head'
+import { Provider } from 'react-redux'
+import { useNewQueryClient } from '../data/query'
+import { useStore } from '../redux'
+import { AuthProvider, useRequireAuth } from '../auth'
+import * as Sentry from '@sentry/react'
+import { Integrations } from '@sentry/tracing'
+import { QueryClientProvider } from 'react-query'
+
+import '@patternfly/react-core/dist/styles/base.css'
+import '@patternfly/react-styles/css/utilities/Alignment/alignment.css'
+import '@patternfly/react-styles/css/utilities/BackgroundColor/BackgroundColor.css'
+import '@patternfly/react-styles/css/utilities/BoxShadow/box-shadow.css'
+import '@patternfly/react-styles/css/utilities/Display/display.css'
+import '@patternfly/react-styles/css/utilities/Flex/flex.css'
+import '@patternfly/react-styles/css/utilities/Float/float.css'
+import '@patternfly/react-styles/css/utilities/Sizing/sizing.css'
+import '@patternfly/react-styles/css/utilities/Spacing/spacing.css'
+import '@patternfly/react-styles/css/utilities/Text/text.css'
+import '@patternfly/react-styles/css/components/InlineEdit/inline-edit.css'
+import '../style/index.scss'
+
+// This setup is necessary to forward the Auth0 context to the Redux context
+function Inner({ Component, pageProps }) {
+ // Force user to be authorized
+ useRequireAuth()
+
+ const queryClient = useNewQueryClient()
+ const store = useStore(pageProps.initialReduxState, { queryClient })
+ return (
+ <QueryClientProvider client={queryClient}>
+ <Provider store={store}>
+ <Component {...pageProps} />
+ </Provider>
+ </QueryClientProvider>
+ )
+}
+
+Inner.propTypes = {
+ Component: PropTypes.func,
+ pageProps: PropTypes.shape({
+ initialReduxState: PropTypes.object,
+ }).isRequired,
+}
+
+const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN
+// Initialize Sentry if the user has configured a DSN
+if (process.browser && dsn) {
+ if (dsn) {
+ Sentry.init({
+ environment: process.env.NODE_ENV,
+ dsn: dsn,
+ integrations: [new Integrations.BrowserTracing()],
+ tracesSampleRate: 0.1,
+ })
+ }
+}
+
+export default function App(props) {
+ return (
+ <>
+ <Head>
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
+ <meta name="theme-color" content="#00A6D6" />
+ </Head>
+ <Sentry.ErrorBoundary fallback={'An error has occurred'}>
+ <AuthProvider>
+ <Inner {...props} />
+ </AuthProvider>
+ </Sentry.ErrorBoundary>
+ </>
+ )
+}
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..51d8d3e0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/_document.js
@@ -0,0 +1,95 @@
+/*
+ * 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 lang="en">
+ <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"
+ />
+
+ {/* 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/logout.js b/opendc-web/opendc-web-ui/src/pages/logout.js
new file mode 100644
index 00000000..38d5968e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/logout.js
@@ -0,0 +1,39 @@
+/*
+ * 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 React from 'react'
+import Head from 'next/head'
+import { AppPage } from '../components/AppPage'
+import { PageSection, PageSectionVariants } from '@patternfly/react-core'
+
+function Logout() {
+ return (
+ <AppPage>
+ <Head>
+ <title>Logged Out - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>Logged out successfully</PageSection>
+ </AppPage>
+ )
+}
+
+export default Logout
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
new file mode 100644
index 00000000..c07a2c31
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
@@ -0,0 +1,83 @@
+/*
+ * 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 ContextSelectionSection from '../../../components/context/ContextSelectionSection'
+import ProjectOverview from '../../../components/projects/ProjectOverview'
+import ProjectSelector from '../../../components/context/ProjectSelector'
+import { useProject } from '../../../data/project'
+import { AppPage } from '../../../components/AppPage'
+import Head from 'next/head'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ PageSection,
+ PageSectionVariants,
+ Skeleton,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+import BreadcrumbLink from '../../../components/util/BreadcrumbLink'
+
+function Project() {
+ const router = useRouter()
+ const { project: projectId } = router.query
+
+ const { data: project } = useProject(projectId)
+
+ const breadcrumb = (
+ <Breadcrumb>
+ <BreadcrumbItem to="/projects" component={BreadcrumbLink}>
+ Projects
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}`} component={BreadcrumbLink} isActive>
+ Project details
+ </BreadcrumbItem>
+ </Breadcrumb>
+ )
+
+ const contextSelectors = (
+ <ContextSelectionSection>
+ <ProjectSelector projectId={projectId} />
+ </ContextSelectionSection>
+ )
+
+ return (
+ <AppPage breadcrumb={breadcrumb} contextSelectors={contextSelectors}>
+ <Head>
+ <title>{project?.name ?? 'Project'} - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">
+ {project?.name ?? <Skeleton width="15%" screenreaderText="Loading project" />}
+ </Text>
+ </TextContent>
+ </PageSection>
+ <PageSection isFilled>
+ <ProjectOverview projectId={projectId} />
+ </PageSection>
+ </AppPage>
+ )
+}
+
+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..d1533d98
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
@@ -0,0 +1,117 @@
+/*
+ * 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 Head from 'next/head'
+import React, { useRef } from 'react'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ Divider,
+ PageSection,
+ PageSectionVariants,
+ Tab,
+ TabContent,
+ Tabs,
+ TabTitleText,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+import { AppPage } from '../../../../components/AppPage'
+import ContextSelectionSection from '../../../../components/context/ContextSelectionSection'
+import PortfolioSelector from '../../../../components/context/PortfolioSelector'
+import ProjectSelector from '../../../../components/context/ProjectSelector'
+import BreadcrumbLink from '../../../../components/util/BreadcrumbLink'
+import PortfolioOverview from '../../../../components/portfolios/PortfolioOverview'
+import PortfolioResults from '../../../../components/portfolios/PortfolioResults'
+
+/**
+ * Page that displays the results in a portfolio.
+ */
+function Portfolio() {
+ const router = useRouter()
+ const { project: projectId, portfolio: portfolioId } = router.query
+
+ const overviewRef = useRef(null)
+ const resultsRef = useRef(null)
+
+ const breadcrumb = (
+ <Breadcrumb>
+ <BreadcrumbItem to="/projects" component={BreadcrumbLink}>
+ Projects
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}`} component={BreadcrumbLink}>
+ Project details
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}/portfolios/${portfolioId}`} component={BreadcrumbLink} isActive>
+ Portfolio
+ </BreadcrumbItem>
+ </Breadcrumb>
+ )
+
+ const contextSelectors = (
+ <ContextSelectionSection>
+ <ProjectSelector projectId={projectId} />
+ <PortfolioSelector projectId={projectId} portfolioId={portfolioId} />
+ </ContextSelectionSection>
+ )
+
+ return (
+ <AppPage breadcrumb={breadcrumb} contextSelectors={contextSelectors}>
+ <Head>
+ <title>Portfolio - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">Portfolio</Text>
+ </TextContent>
+ </PageSection>
+ <PageSection type="none" variant={PageSectionVariants.light} className="pf-c-page__main-tabs" sticky="top">
+ <Divider component="div" />
+ <Tabs defaultActiveKey={0} className="pf-m-page-insets">
+ <Tab
+ eventKey={0}
+ title={<TabTitleText>Overview</TabTitleText>}
+ tabContentId="overview"
+ tabContentRef={overviewRef}
+ />
+ <Tab
+ eventKey={1}
+ title={<TabTitleText>Results</TabTitleText>}
+ tabContentId="results"
+ tabContentRef={resultsRef}
+ />
+ </Tabs>
+ </PageSection>
+ <PageSection isFilled>
+ <TabContent eventKey={0} id="overview" ref={overviewRef} aria-label="Overview tab">
+ <PortfolioOverview portfolioId={portfolioId} />
+ </TabContent>
+ <TabContent eventKey={1} id="results" ref={resultsRef} aria-label="Results tab" hidden>
+ <PortfolioResults portfolioId={portfolioId} />
+ </TabContent>
+ </PageSection>
+ </AppPage>
+ )
+}
+
+export default Portfolio
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
new file mode 100644
index 00000000..858f9b16
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
@@ -0,0 +1,140 @@
+/*
+ * 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 ContextSelectionSection from '../../../../components/context/ContextSelectionSection'
+import ProjectSelector from '../../../../components/context/ProjectSelector'
+import TopologySelector from '../../../../components/context/TopologySelector'
+import TopologyOverview from '../../../../components/topologies/TopologyOverview'
+import { useProject } from '../../../../data/project'
+import { useDispatch } from 'react-redux'
+import React, { useEffect, useState } from 'react'
+import Head from 'next/head'
+import { AppPage } from '../../../../components/AppPage'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ Divider,
+ PageSection,
+ PageSectionVariants,
+ Tab,
+ TabContent,
+ Tabs,
+ TabTitleText,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+import BreadcrumbLink from '../../../../components/util/BreadcrumbLink'
+import TopologyMap from '../../../../components/topologies/TopologyMap'
+import { goToRoom } from '../../../../redux/actions/interaction-level'
+import { openTopology } from '../../../../redux/actions/topologies'
+
+/**
+ * Page that displays a datacenter topology.
+ */
+function Topology() {
+ const router = useRouter()
+ const { project: projectId, topology: topologyId } = router.query
+
+ const { data: project } = useProject(projectId)
+
+ const dispatch = useDispatch()
+ useEffect(() => {
+ if (topologyId) {
+ dispatch(openTopology(topologyId))
+ }
+ }, [topologyId, dispatch])
+
+ const [activeTab, setActiveTab] = useState('overview')
+
+ const breadcrumb = (
+ <Breadcrumb>
+ <BreadcrumbItem to="/projects" component={BreadcrumbLink}>
+ Projects
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}`} component={BreadcrumbLink}>
+ Project details
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}/topologies/${topologyId}`} component={BreadcrumbLink} isActive>
+ Topology
+ </BreadcrumbItem>
+ </Breadcrumb>
+ )
+
+ const contextSelectors = (
+ <ContextSelectionSection>
+ <ProjectSelector projectId={projectId} />
+ <TopologySelector projectId={projectId} topologyId={topologyId} />
+ </ContextSelectionSection>
+ )
+
+ return (
+ <AppPage breadcrumb={breadcrumb} contextSelectors={contextSelectors}>
+ <Head>
+ <title>{project?.name ?? 'Topologies'} - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">Topology</Text>
+ </TextContent>
+ </PageSection>
+ <PageSection type="none" variant={PageSectionVariants.light} className="pf-c-page__main-tabs" sticky="top">
+ <Divider component="div" />
+ <Tabs
+ activeKey={activeTab}
+ onSelect={(_, tabIndex) => setActiveTab(tabIndex)}
+ className="pf-m-page-insets"
+ >
+ <Tab eventKey="overview" title={<TabTitleText>Overview</TabTitleText>} tabContentId="overview" />
+ <Tab
+ eventKey="floor-plan"
+ title={<TabTitleText>Floor Plan</TabTitleText>}
+ tabContentId="floor-plan"
+ />
+ </Tabs>
+ </PageSection>
+ <PageSection padding={activeTab === 'floor-plan' && { default: 'noPadding' }} isFilled>
+ <TabContent id="overview" aria-label="Overview tab" hidden={activeTab !== 'overview'}>
+ <TopologyOverview
+ topologyId={topologyId}
+ onSelect={(type, obj) => {
+ if (type === 'room') {
+ dispatch(goToRoom(obj._id))
+ setActiveTab('floor-plan')
+ }
+ }}
+ />
+ </TabContent>
+ <TabContent
+ id="floor-plan"
+ aria-label="Floor Plan tab"
+ className="pf-u-h-100"
+ hidden={activeTab !== 'floor-plan'}
+ >
+ <TopologyMap />
+ </TabContent>
+ </PageSection>
+ </AppPage>
+ )
+}
+
+export default Topology
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..eb77701e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js
@@ -0,0 +1,87 @@
+/*
+ * 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 React, { useMemo, useState } from 'react'
+import Head from 'next/head'
+import ProjectFilterPanel from '../../components/projects/FilterPanel'
+import { useAuth } from '../../auth'
+import { AppPage } from '../../components/AppPage'
+import { PageSection, PageSectionVariants, Text, TextContent } from '@patternfly/react-core'
+import { useProjects } from '../../data/project'
+import ProjectTable from '../../components/projects/ProjectTable'
+import { useMutation } from 'react-query'
+import NewProject from '../../components/projects/NewProject'
+
+const getVisibleProjects = (projects, filter, userId) => {
+ switch (filter) {
+ case 'SHOW_ALL':
+ return projects
+ case 'SHOW_OWN':
+ return projects.filter((project) =>
+ project.authorizations.some((a) => a.userId === userId && a.level === 'OWN')
+ )
+ case 'SHOW_SHARED':
+ return projects.filter((project) =>
+ project.authorizations.some((a) => a.userId === userId && a.level !== 'OWN')
+ )
+ default:
+ return projects
+ }
+}
+
+function Projects() {
+ const { user } = useAuth()
+ const { status, data: projects } = useProjects()
+ const [filter, setFilter] = useState('SHOW_ALL')
+ const visibleProjects = useMemo(() => getVisibleProjects(projects ?? [], filter, user?.sub), [
+ projects,
+ filter,
+ user?.sub,
+ ])
+
+ const { mutate: deleteProject } = useMutation('deleteProject')
+
+ return (
+ <AppPage>
+ <Head>
+ <title>My Projects - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">My Projects</Text>
+ </TextContent>
+ </PageSection>
+ <PageSection variant={PageSectionVariants.light} isFilled>
+ <ProjectFilterPanel onSelect={setFilter} activeFilter={filter} />
+ <ProjectTable
+ status={status}
+ isFiltering={filter !== 'SHOW_ALL'}
+ projects={visibleProjects}
+ onDelete={(project) => deleteProject(project._id)}
+ />
+ <NewProject />
+ </PageSection>
+ </AppPage>
+ )
+}
+
+export default Projects