diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-07-16 10:32:57 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-07-16 10:32:57 +0200 |
| commit | db1d2c2f8c18850dedf34b5d690b6cd6a1d1f6b5 (patch) | |
| tree | 263a6f9741c5ca0dd64ecf3f7f07b580331aec9d /opendc-web/opendc-web-ui/src/pages/projects | |
| parent | 1a2416043f0b877f570e89da74e0d0a4aff1d8ae (diff) | |
| parent | 803e13b32cf0ff8b496649fb0a4d6e32400e98a4 (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/projects')
4 files changed, 383 insertions, 61 deletions
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> ) } |
