summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src/pages
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-07-16 10:32:57 +0200
committerGitHub <noreply@github.com>2021-07-16 10:32:57 +0200
commitdb1d2c2f8c18850dedf34b5d690b6cd6a1d1f6b5 (patch)
tree263a6f9741c5ca0dd64ecf3f7f07b580331aec9d /opendc-web/opendc-web-ui/src/pages
parent1a2416043f0b877f570e89da74e0d0a4aff1d8ae (diff)
parent803e13b32cf0ff8b496649fb0a4d6e32400e98a4 (diff)
merge: Add PatternFly 4 web interface (#161)
This pull requests adds the new web interface based on the PatternFly 4 design framework. This framework enables us to develop more quickly the interfaces necessary in OpenDC. * Remove the OpenDC landing page from the web interface module * Add support for the PatternFly 4 framework in Next.js * Relax topology schema requirements * Migrate UI components to PatternFly 4
Diffstat (limited to 'opendc-web/opendc-web-ui/src/pages')
-rw-r--r--opendc-web/opendc-web-ui/src/pages/404.js33
-rw-r--r--opendc-web/opendc-web-ui/src/pages/404.module.scss8
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_app.js14
-rw-r--r--opendc-web/opendc-web-ui/src/pages/index.js42
-rw-r--r--opendc-web/opendc-web-ui/src/pages/index.module.scss16
-rw-r--r--opendc-web/opendc-web-ui/src/pages/logout.js10
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js111
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js170
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js73
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/index.js90
10 files changed, 427 insertions, 140 deletions
diff --git a/opendc-web/opendc-web-ui/src/pages/404.js b/opendc-web/opendc-web-ui/src/pages/404.js
index cc9281fc..0939bc56 100644
--- a/opendc-web/opendc-web-ui/src/pages/404.js
+++ b/opendc-web/opendc-web-ui/src/pages/404.js
@@ -1,18 +1,37 @@
import React from 'react'
import Head from 'next/head'
-import TerminalWindow from '../components/not-found/TerminalWindow'
-import style from './404.module.scss'
+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>
- <div className={style['not-found-backdrop']}>
- <TerminalWindow />
- </div>
- </>
+ <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>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/404.module.scss b/opendc-web/opendc-web-ui/src/pages/404.module.scss
deleted file mode 100644
index e91c2780..00000000
--- a/opendc-web/opendc-web-ui/src/pages/404.module.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.not-found-backdrop {
- display: flex;
-
- width: 100%;
- height: 100%;
-
- background-image: linear-gradient(135deg, #00678a, #008fbf, #00a6d6);
-}
diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js
index 6a7200d5..028dab10 100644
--- a/opendc-web/opendc-web-ui/src/pages/_app.js
+++ b/opendc-web/opendc-web-ui/src/pages/_app.js
@@ -24,7 +24,6 @@ import PropTypes from 'prop-types'
import Head from 'next/head'
import { Provider } from 'react-redux'
import { useStore } from '../redux'
-import '../index.scss'
import { AuthProvider, useAuth } from '../auth'
import * as Sentry from '@sentry/react'
import { Integrations } from '@sentry/tracing'
@@ -34,6 +33,19 @@ import { configureProjectClient } from '../data/project'
import { configureExperimentClient } from '../data/experiments'
import { configureTopologyClient } from '../data/topology'
+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
const Inner = ({ Component, pageProps }) => {
const auth = useAuth()
diff --git a/opendc-web/opendc-web-ui/src/pages/index.js b/opendc-web/opendc-web-ui/src/pages/index.js
deleted file mode 100644
index bb904eb6..00000000
--- a/opendc-web/opendc-web-ui/src/pages/index.js
+++ /dev/null
@@ -1,42 +0,0 @@
-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'
-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 {
- introSection,
- stakeholderSection,
- modelingSection,
- simulationSection,
- technologiesSection,
- teamSection,
-} from './index.module.scss'
-
-function Home() {
- return (
- <>
- <Head>
- <title>OpenDC</title>
- </Head>
- <HomeNavbar />
- <div className="body-wrapper page-container">
- <JumbotronHeader />
- <IntroSection className={introSection} />
- <StakeholderSection className={stakeholderSection} />
- <ModelingSection className={modelingSection} />
- <SimulationSection className={simulationSection} />
- <TechnologiesSection className={technologiesSection} />
- <TeamSection className={teamSection} />
- <ContactSection />
- </div>
- </>
- )
-}
-
-export default Home
diff --git a/opendc-web/opendc-web-ui/src/pages/index.module.scss b/opendc-web/opendc-web-ui/src/pages/index.module.scss
deleted file mode 100644
index aed1d88f..00000000
--- a/opendc-web/opendc-web-ui/src/pages/index.module.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-.bodyWrapper {
- position: relative;
- overflow-y: hidden;
-}
-
-.introSection,
-.modelingSection,
-.technologiesSection {
- background-color: #fff;
-}
-
-.stakeholderSection,
-.simulationSection,
-.teamSection {
- background-color: #f2f2f2;
-}
diff --git a/opendc-web/opendc-web-ui/src/pages/logout.js b/opendc-web/opendc-web-ui/src/pages/logout.js
index e96e0605..38d5968e 100644
--- a/opendc-web/opendc-web-ui/src/pages/logout.js
+++ b/opendc-web/opendc-web-ui/src/pages/logout.js
@@ -22,17 +22,17 @@
import React from 'react'
import Head from 'next/head'
-import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
+import { AppPage } from '../components/AppPage'
+import { PageSection, PageSectionVariants } from '@patternfly/react-core'
function Logout() {
return (
- <>
+ <AppPage>
<Head>
<title>Logged Out - OpenDC</title>
</Head>
- <AppNavbarContainer fullWidth={false} />
- <span>Logged out successfully</span>
- </>
+ <PageSection variant={PageSectionVariants.light}>Logged out successfully</PageSection>
+ </AppPage>
)
}
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
index cce887aa..c6ded12b 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
@@ -20,6 +20,113 @@
* SOFTWARE.
*/
-import Topology from './topologies/[topology]'
+import { useRouter } from 'next/router'
+import { useProject } from '../../../data/project'
+import { AppPage } from '../../../components/AppPage'
+import Head from 'next/head'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ Button,
+ Card,
+ CardActions,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ Grid,
+ GridItem,
+ PageSection,
+ PageSectionVariants,
+ Skeleton,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+import BreadcrumbLink from '../../../components/util/BreadcrumbLink'
+import PortfolioTable from '../../../components/projects/PortfolioTable'
+import TopologyTable from '../../../components/projects/TopologyTable'
+import NewTopology from '../../../components/projects/NewTopology'
+import NewPortfolio from '../../../components/projects/NewPortfolio'
-export default Topology
+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>
+ )
+
+ return (
+ <AppPage breadcrumb={breadcrumb}>
+ <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>
+ <Grid hasGutter>
+ <GridItem md={2}>
+ <Card>
+ <CardTitle>Details</CardTitle>
+ <CardBody>
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Name</DescriptionListTerm>
+ <DescriptionListDescription>
+ {project?.name ?? <Skeleton screenreaderText="Loading project" />}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ </CardBody>
+ </Card>
+ </GridItem>
+ <GridItem md={5}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <NewTopology projectId={projectId} />
+ </CardActions>
+ <CardTitle>Topologies</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <TopologyTable projectId={projectId} />
+ </CardBody>
+ </Card>
+ </GridItem>
+ <GridItem md={5}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <NewPortfolio projectId={projectId} />
+ </CardActions>
+ <CardTitle>Portfolios</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <PortfolioTable projectId={projectId} />
+ </CardBody>
+ </Card>
+ </GridItem>
+ </Grid>
+ </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
index d3d61271..55bee445 100644
--- 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
@@ -22,12 +22,41 @@
import { useRouter } from 'next/router'
import Head from 'next/head'
-import AppNavbarContainer from '../../../../containers/navigation/AppNavbarContainer'
-import React from 'react'
-import { useProject } from '../../../../data/project'
-import ProjectSidebarContainer from '../../../../containers/app/sidebars/project/ProjectSidebarContainer'
-import PortfolioResultsContainer from '../../../../containers/app/results/PortfolioResultsContainer'
-import { useDispatch } from 'react-redux'
+import React, { useRef } from 'react'
+import { usePortfolio, useProject } from '../../../../data/project'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ Card,
+ CardActions,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ Chip,
+ ChipGroup,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ Divider,
+ Grid,
+ GridItem,
+ PageSection,
+ PageSectionVariants,
+ Skeleton,
+ Tab,
+ TabContent,
+ Tabs,
+ TabTitleText,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+import { AppPage } from '../../../../components/AppPage'
+import BreadcrumbLink from '../../../../components/util/BreadcrumbLink'
+import ScenarioTable from '../../../../components/projects/ScenarioTable'
+import NewScenario from '../../../../components/projects/NewScenario'
+import { METRIC_NAMES } from '../../../../util/available-metrics'
+import PortfolioResults from '../../../../components/app/results/PortfolioResults'
/**
* Page that displays the results in a portfolio.
@@ -36,24 +65,127 @@ function Portfolio() {
const router = useRouter()
const { project: projectId, portfolio: portfolioId } = router.query
- const project = useProject(projectId)
- const title = project?.name ? project?.name + ' - OpenDC' : 'Simulation - OpenDC'
+ const { data: project } = useProject(projectId)
+ const { data: portfolio } = usePortfolio(portfolioId)
- const dispatch = useDispatch()
+ 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>
+ )
return (
- <div className="page-container full-height">
+ <AppPage breadcrumb={breadcrumb}>
<Head>
- <title>{title}</title>
+ <title>{project?.name ?? 'Portfolios'} - OpenDC</title>
</Head>
- <AppNavbarContainer fullWidth={true} />
- <div className="full-height app-page-container">
- <ProjectSidebarContainer />
- <div className="container-fluid full-height">
- <PortfolioResultsContainer />
- </div>
- </div>
- </div>
+ <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">
+ <Grid hasGutter>
+ <GridItem md={2}>
+ <Card>
+ <CardTitle>Details</CardTitle>
+ <CardBody>
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Name</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.name ?? <Skeleton screenreaderText="Loading portfolio" />}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Scenarios</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.scenarioIds.length ?? (
+ <Skeleton screenreaderText="Loading portfolio" />
+ )}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Metrics</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.targets?.enabledMetrics ? (
+ portfolio.targets.enabledMetrics.length > 0 ? (
+ <ChipGroup>
+ {portfolio.targets.enabledMetrics.map((metric) => (
+ <Chip isReadOnly key={metric}>
+ {METRIC_NAMES[metric]}
+ </Chip>
+ ))}
+ </ChipGroup>
+ ) : (
+ 'No metrics enabled'
+ )
+ ) : (
+ <Skeleton screenreaderText="Loading portfolio" />
+ )}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Repeats per Scenario</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.targets?.repeatsPerScenario ?? (
+ <Skeleton screenreaderText="Loading portfolio" />
+ )}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ </CardBody>
+ </Card>
+ </GridItem>
+ <GridItem md={6}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <NewScenario portfolioId={portfolioId} />
+ </CardActions>
+ <CardTitle>Scenarios</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <ScenarioTable portfolioId={portfolioId} />
+ </CardBody>
+ </Card>
+ </GridItem>
+ </Grid>
+ </TabContent>
+ <TabContent eventKey={1} id="results" ref={resultsRef} aria-label="Results tab" hidden>
+ <PortfolioResults portfolioId={portfolioId} />
+ </TabContent>
+ </PageSection>
+ </AppPage>
)
}
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
index a9dfdb19..5873ed11 100644
--- 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
@@ -23,18 +23,29 @@
import { useRouter } from 'next/router'
import { useProject } from '../../../../data/project'
import { useDispatch, useSelector } from 'react-redux'
-import React, { useEffect } from 'react'
+import React, { useEffect, useState } from 'react'
import { HotKeys } from 'react-hotkeys'
import { KeymapConfiguration } from '../../../../hotkeys'
import Head from 'next/head'
-import AppNavbarContainer from '../../../../containers/navigation/AppNavbarContainer'
-import LoadingScreen from '../../../../components/app/map/LoadingScreen'
-import MapStage from '../../../../containers/app/map/MapStage'
-import ScaleIndicatorContainer from '../../../../containers/app/map/controls/ScaleIndicatorContainer'
-import ToolPanelComponent from '../../../../components/app/map/controls/ToolPanelComponent'
-import ProjectSidebarContainer from '../../../../containers/app/sidebars/project/ProjectSidebarContainer'
-import TopologySidebarContainer from '../../../../containers/app/sidebars/topology/TopologySidebarContainer'
+import MapStage from '../../../../components/app/map/MapStage'
import { openProjectSucceeded } from '../../../../redux/actions/projects'
+import { AppPage } from '../../../../components/AppPage'
+import {
+ Bullseye,
+ Drawer,
+ DrawerContent,
+ DrawerContentBody,
+ EmptyState,
+ EmptyStateIcon,
+ Spinner,
+ Title,
+} from '@patternfly/react-core'
+import { zoomInOnCenter } from '../../../../redux/actions/map'
+import Toolbar from '../../../../components/app/map/controls/Toolbar'
+import { useMapScale } from '../../../../data/map'
+import ScaleIndicator from '../../../../components/app/map/controls/ScaleIndicator'
+import TopologySidebar from '../../../../components/app/sidebars/topology/TopologySidebar'
+import Collapse from '../../../../components/app/map/controls/Collapse'
/**
* Page that displays a datacenter topology.
@@ -44,7 +55,6 @@ function Topology() {
const { project: projectId, topology: topologyId } = router.query
const { data: project } = useProject(projectId)
- const title = project?.name ? project?.name + ' - OpenDC' : 'Simulation - OpenDC'
const dispatch = useDispatch()
useEffect(() => {
@@ -54,27 +64,44 @@ function Topology() {
}, [projectId, topologyId, dispatch])
const topologyIsLoading = useSelector((state) => state.currentTopologyId === '-1')
+ const scale = useMapScale()
+ const interactionLevel = useSelector((state) => state.interactionLevel)
+
+ const [isExpanded, setExpanded] = useState(true)
+ const panelContent = <TopologySidebar interactionLevel={interactionLevel} onClose={() => setExpanded(false)} />
return (
- <HotKeys keyMap={KeymapConfiguration} allowChanges={true} className="page-container full-height">
+ <AppPage>
<Head>
- <title>{title}</title>
+ <title>{project?.name ?? 'Topologies'} - OpenDC</title>
</Head>
- <AppNavbarContainer fullWidth={true} />
{topologyIsLoading ? (
- <div className="full-height d-flex align-items-center justify-content-center">
- <LoadingScreen />
- </div>
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={Spinner} />
+ <Title size="lg" headingLevel="h4">
+ Loading Topology
+ </Title>
+ </EmptyState>
+ </Bullseye>
) : (
- <div className="full-height">
- <MapStage />
- <ScaleIndicatorContainer />
- <ToolPanelComponent />
- <ProjectSidebarContainer />
- <TopologySidebarContainer />
- </div>
+ <HotKeys keyMap={KeymapConfiguration} allowChanges={true} className="full-height">
+ <Drawer isExpanded={isExpanded}>
+ <DrawerContent panelContent={panelContent}>
+ <DrawerContentBody>
+ <MapStage />
+ <ScaleIndicator scale={scale} />
+ <Toolbar
+ onZoom={(zoomIn) => dispatch(zoomInOnCenter(zoomIn))}
+ onExport={() => window['exportCanvasToImage']()}
+ />
+ <Collapse onClick={() => setExpanded(true)} />
+ </DrawerContentBody>
+ </DrawerContent>
+ </Drawer>
+ </HotKeys>
)}
- </HotKeys>
+ </AppPage>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/index.js b/opendc-web/opendc-web-ui/src/pages/projects/index.js
index 2d8e6de7..9dcc9aea 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/index.js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js
@@ -1,31 +1,87 @@
-import React, { useState } from 'react'
+/*
+ * 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 NewProjectContainer from '../../containers/projects/NewProjectContainer'
-import ProjectListContainer from '../../containers/projects/ProjectListContainer'
-import AppNavbarContainer from '../../containers/navigation/AppNavbarContainer'
-import { useRequireAuth } from '../../auth'
-import { Container } from 'reactstrap'
+import { useAuth, useRequireAuth } 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() {
useRequireAuth()
-
+ 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>
- <div className="full-height">
- <AppNavbarContainer fullWidth={false} />
- <Container className="text-page-container">
- <ProjectFilterPanel onSelect={setFilter} activeFilter={filter} />
- <ProjectListContainer filter={filter} />
- <NewProjectContainer />
- </Container>
- </div>
- </>
+ <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>
)
}