diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-07-14 22:23:40 +0200 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-07-15 15:55:56 +0200 |
| commit | 803e13b32cf0ff8b496649fb0a4d6e32400e98a4 (patch) | |
| tree | 263a6f9741c5ca0dd64ecf3f7f07b580331aec9d /opendc-web/opendc-web-ui/src | |
| parent | e200dbfdc076ac6263c9ac6f9dabdcc475f01d6e (diff) | |
feat(ui): Migrate to PatternFly 4 design framework
This change is a rewrite of the existing OpenDC frontend in order to
migrate to the PatternFly 4 design framework.
PatternFly is used by Red Hat for various computing related services such
as OpenShift, Red Hat Virtualization and Cockpit. Since their design
requirements are very similar to those of OpenDC (modeling computing
services), migrating to PatternFly 4 allows us to re-use design choices
from these services.
See https://www.patternfly.org/v4/ for more information about
PatternFly.
Diffstat (limited to 'opendc-web/opendc-web-ui/src')
187 files changed, 4246 insertions, 3063 deletions
diff --git a/opendc-web/opendc-web-ui/src/auth.js b/opendc-web/opendc-web-ui/src/auth.js index 96417e67..e670476c 100644 --- a/opendc-web/opendc-web-ui/src/auth.js +++ b/opendc-web/opendc-web-ui/src/auth.js @@ -23,7 +23,6 @@ import PropTypes from 'prop-types' import { Auth0Provider, useAuth0 } from '@auth0/auth0-react' import { useEffect } from 'react' -import { useRouter } from 'next/router' /** * Obtain the authentication context. @@ -37,14 +36,13 @@ export function useAuth() { */ export function useRequireAuth() { const auth = useAuth() - const router = useRouter() - const { isLoading, isAuthenticated } = auth + const { loginWithRedirect, isLoading, isAuthenticated } = auth useEffect(() => { if (!isLoading && !isAuthenticated) { - router.replace('/') + loginWithRedirect() } - }, [router, isLoading, isAuthenticated]) + }, [loginWithRedirect, isLoading, isAuthenticated]) return auth } diff --git a/opendc-web/opendc-web-ui/src/components/AppHeader.js b/opendc-web/opendc-web-ui/src/components/AppHeader.js new file mode 100644 index 00000000..b33212c4 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/AppHeader.js @@ -0,0 +1,44 @@ +/* + * 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 { PageHeader } from '@patternfly/react-core' +import React from 'react' +import Image from 'next/image' +import AppHeaderTools from './AppHeaderTools' +import { AppNavigation } from './AppNavigation' +import AppLogo from './AppLogo' + +export function AppHeader() { + const logo = <Image src="/img/logo.png" layout="fixed" width={30} height={30} alt="OpenDC" /> + + return ( + <PageHeader + logo={logo} + logoProps={{ href: '/' }} + logoComponent={AppLogo} + headerTools={<AppHeaderTools />} + topNav={<AppNavigation />} + /> + ) +} + +AppHeader.propTypes = {} diff --git a/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js new file mode 100644 index 00000000..02e5d265 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js @@ -0,0 +1,133 @@ +/* + * 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 { + Avatar, + Button, + ButtonVariant, + Dropdown, + DropdownGroup, + DropdownItem, + DropdownToggle, + KebabToggle, + PageHeaderTools, + PageHeaderToolsGroup, + PageHeaderToolsItem, + Skeleton, +} from '@patternfly/react-core' +import { useState } from 'react' +import { useAuth } from '../auth' +import { GithubIcon, HelpIcon } from '@patternfly/react-icons' + +function AppHeaderTools() { + const auth = useAuth() + + const [isKebabDropdownOpen, setKebabDropdownOpen] = useState(false) + const kebabDropdownItems = [ + <DropdownItem + key={0} + component={ + <a href="https://opendc.org" target="_blank" rel="noreferrer"> + <HelpIcon /> Help + </a> + } + />, + ] + + const [isDropdownOpen, setDropdownOpen] = useState(false) + const userDropdownItems = [ + <DropdownGroup key="group 2"> + <DropdownItem key="group 2 logout" onClick={() => auth.logout({ returnTo: window.location.origin })}> + Logout + </DropdownItem> + </DropdownGroup>, + ] + + return ( + <PageHeaderTools> + <PageHeaderToolsGroup visibility={{ default: 'hidden', lg: 'visible' }}> + <PageHeaderToolsItem> + <Button + component="a" + href="https://github.com/atlarge-research/opendc" + target="_blank" + aria-label="Source code" + variant={ButtonVariant.plain} + > + <GithubIcon /> + </Button> + </PageHeaderToolsItem> + <PageHeaderToolsItem> + <Button + component="a" + href="https://opendc.org/" + target="_blank" + aria-label="Help actions" + variant={ButtonVariant.plain} + > + <HelpIcon /> + </Button> + </PageHeaderToolsItem> + </PageHeaderToolsGroup> + <PageHeaderToolsGroup> + <PageHeaderToolsItem visibility={{ lg: 'hidden' }}> + <Dropdown + isPlain + position="right" + toggle={<KebabToggle onToggle={() => setKebabDropdownOpen(!isKebabDropdownOpen)} />} + isOpen={isKebabDropdownOpen} + dropdownItems={kebabDropdownItems} + /> + </PageHeaderToolsItem> + <PageHeaderToolsItem visibility={{ default: 'hidden', md: 'visible' }}> + <Dropdown + isPlain + position="right" + isOpen={isDropdownOpen} + toggle={ + <DropdownToggle onToggle={() => setDropdownOpen(!isDropdownOpen)}> + {auth?.user?.name ?? ( + <Skeleton + fontSize="xs" + width="150px" + className="pf-u-display-inline-flex" + screenreaderText="Loading username" + /> + )} + </DropdownToggle> + } + dropdownItems={userDropdownItems} + /> + </PageHeaderToolsItem> + </PageHeaderToolsGroup> + {auth?.user?.picture ? ( + <Avatar src={auth.user.picture} alt="Avatar image" /> + ) : ( + <Skeleton className="pf-c-avatar" shape="circle" width="2.25rem" screenreaderText="Loading avatar" /> + )} + </PageHeaderTools> + ) +} + +AppHeaderTools.propTypes = {} + +export default AppHeaderTools diff --git a/opendc-web/opendc-web-ui/src/components/AppLogo.js b/opendc-web/opendc-web-ui/src/components/AppLogo.js new file mode 100644 index 00000000..92663295 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/AppLogo.js @@ -0,0 +1,46 @@ +/* + * 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 Link from 'next/link' +import { appLogo } from './AppLogo.module.scss' + +function AppLogo({ href, children, className, ...props }) { + return ( + <> + <Link href={href}> + <a {...props} className={`${className ?? ''} ${appLogo}`}> + {children} + <span>OpenDC</span> + </a> + </Link> + </> + ) +} + +AppLogo.propTypes = { + href: PropTypes.string.isRequired, + children: PropTypes.node, + className: PropTypes.string, +} + +export default AppLogo diff --git a/opendc-web/opendc-web-ui/src/components/AppLogo.module.scss b/opendc-web/opendc-web-ui/src/components/AppLogo.module.scss new file mode 100644 index 00000000..3d228cb6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/AppLogo.module.scss @@ -0,0 +1,33 @@ +/*! + * 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. + */ + +.appLogo { + span { + margin-left: 4px; + color: #fff; + } + + &:hover, + &:focus { + text-decoration: none; + } +} diff --git a/opendc-web/opendc-web-ui/src/components/AppNavigation.js b/opendc-web/opendc-web-ui/src/components/AppNavigation.js new file mode 100644 index 00000000..b3f11f34 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/AppNavigation.js @@ -0,0 +1,75 @@ +/* + * 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 { Dropdown, DropdownItem, DropdownToggle, Nav, NavItem, NavList } from '@patternfly/react-core' +import { useRouter } from 'next/router' +import NavItemLink from './util/NavItemLink' +import { useProject } from '../data/project' + +export function AppNavigation() { + const { pathname, query } = useRouter() + const { project: projectId } = query + const { data: project } = useProject(projectId) + + const nextTopologyId = project?.topologyIds?.[0] + const nextPortfolioId = project?.portfolioIds?.[0] + + return ( + <Nav variant="horizontal"> + <NavList> + <NavItem + id="projects" + to="/projects" + itemId={0} + component={NavItemLink} + isActive={pathname === '/projects' || pathname === '/projects/[project]'} + > + Projects + </NavItem> + {pathname.startsWith('/projects/[project]') && ( + <> + <NavItem + id="topologies" + to={nextTopologyId ? `/projects/${projectId}/topologies/${nextTopologyId}` : '/projects'} + itemId={1} + component={NavItemLink} + isActive={pathname === '/projects/[project]/topologies/[topology]'} + > + Topologies + </NavItem> + <NavItem + id="portfolios" + to={nextPortfolioId ? `/projects/${projectId}/portfolios/${nextPortfolioId}` : '/projects'} + itemId={2} + component={NavItemLink} + isActive={pathname === '/projects/[project]/portfolios/[portfolio]'} + > + Portfolios + </NavItem> + </> + )} + </NavList> + </Nav> + ) +} + +AppNavigation.propTypes = {} diff --git a/opendc-web/opendc-web-ui/src/components/AppPage.js b/opendc-web/opendc-web-ui/src/components/AppPage.js new file mode 100644 index 00000000..7cf9cc15 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/AppPage.js @@ -0,0 +1,41 @@ +/* + * 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 { AppHeader } from './AppHeader' +import { AppNavigation } from './AppNavigation' +import React, { useState } from 'react' +import { Page } from '@patternfly/react-core' + +export function AppPage({ children, breadcrumb, tertiaryNav }) { + return ( + <Page breadcrumb={breadcrumb} tertiaryNav={tertiaryNav} header={<AppHeader />}> + {children} + </Page> + ) +} + +AppPage.propTypes = { + breadcrumb: PropTypes.node, + tertiaryNav: PropTypes.node, + children: PropTypes.node, +} diff --git a/opendc-web/opendc-web-ui/src/components/app/map/GrayContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/GrayContainer.js new file mode 100644 index 00000000..4791940f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/GrayContainer.js @@ -0,0 +1,34 @@ +/* + * 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 { useDispatch } from 'react-redux' +import { goDownOneInteractionLevel } from '../../../redux/actions/interaction-level' +import GrayLayer from '../../../components/app/map/elements/GrayLayer' + +const GrayContainer = () => { + const dispatch = useDispatch() + const onClick = () => dispatch(goDownOneInteractionLevel()) + return <GrayLayer onClick={onClick} /> +} + +export default GrayContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js b/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js deleted file mode 100644 index ddb94990..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faSpinner } from '@fortawesome/free-solid-svg-icons' - -const LoadingScreen = () => ( - <div className="display-4"> - <FontAwesomeIcon icon={faSpinner} spin className="mr-4" /> - Loading your project... - </div> -) - -export default LoadingScreen diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js index d6ea1f84..45799f70 100644 --- a/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js +++ b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js @@ -8,8 +8,8 @@ export const TILE_PLUS_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 3 export const OBJECT_SIZE_IN_PIXELS = TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2 export const GRID_LINE_WIDTH_IN_PIXELS = 2 -export const WALL_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 8 -export const OBJECT_BORDER_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 12 +export const WALL_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 16 +export const OBJECT_BORDER_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 16 export const TILE_PLUS_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 10 export const SIDEBAR_WIDTH = 350 diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapStage.js b/opendc-web/opendc-web-ui/src/components/app/map/MapStage.js new file mode 100644 index 00000000..73accf3f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/MapStage.js @@ -0,0 +1,66 @@ +import React, { useEffect, useRef, useState } from 'react' +import { HotKeys } from 'react-hotkeys' +import { Stage } from 'react-konva' +import { MAP_MOVE_PIXELS_PER_EVENT } from './MapConstants' +import { Provider, useDispatch, useStore } from 'react-redux' +import useResizeObserver from 'use-resize-observer' +import { mapContainer } from './MapStage.module.scss' +import { useMapPosition } from '../../../data/map' +import { setMapDimensions, setMapPositionWithBoundsCheck, zoomInOnPosition } from '../../../redux/actions/map' +import MapLayer from './layers/MapLayer' +import RoomHoverLayer from './layers/RoomHoverLayer' +import ObjectHoverLayer from './layers/ObjectHoverLayer' + +function MapStage() { + const store = useStore() + const dispatch = useDispatch() + + const stage = useRef(null) + const [pos, setPos] = useState([0, 0]) + 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), + } + const mapPosition = useMapPosition() + const { ref, width = 100, height = 100 } = useResizeObserver() + + const moveWithDelta = (deltaX, deltaY) => + dispatch(setMapPositionWithBoundsCheck(mapPosition.x + deltaX, mapPosition.y + deltaY)) + const updateMousePosition = () => { + if (!stage.current) { + return + } + + const mousePos = stage.current.getStage().getPointerPosition() + setPos([mousePos.x, mousePos.y]) + } + const updateScale = ({ evt }) => dispatch(zoomInOnPosition(evt.deltaY < 0, x, y)) + + useEffect(() => { + window['exportCanvasToImage'] = () => { + const download = document.createElement('a') + download.href = stage.current.getStage().toDataURL() + download.download = 'opendc-canvas-export-' + Date.now() + '.png' + download.click() + } + }, [stage]) + + useEffect(() => dispatch(setMapDimensions(width, height)), [width, height]) // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <HotKeys handlers={handlers} allowChanges={true} innerRef={ref} className={mapContainer}> + <Stage ref={stage} width={width} height={height} onMouseMove={updateMousePosition} onWheel={updateScale}> + <Provider store={store}> + <MapLayer /> + <RoomHoverLayer mouseX={x} mouseY={y} /> + <ObjectHoverLayer mouseX={x} mouseY={y} /> + </Provider> + </Stage> + </HotKeys> + ) +} + +export default MapStage diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapStage.module.scss b/opendc-web/opendc-web-ui/src/components/app/map/MapStage.module.scss new file mode 100644 index 00000000..d879b4c8 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/MapStage.module.scss @@ -0,0 +1,31 @@ +/*! + * 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. + */ + +.mapContainer { + background-color: var(--pf-global--Color--light-200); + position: relative; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} 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 deleted file mode 100644 index c3177fe1..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js +++ /dev/null @@ -1,97 +0,0 @@ -import PropTypes from 'prop-types' -import React, { useEffect, useRef, useState } from 'react' -import { HotKeys } from 'react-hotkeys' -import { Stage } from 'react-konva' -import MapLayer from '../../../containers/app/map/layers/MapLayer' -import ObjectHoverLayer from '../../../containers/app/map/layers/ObjectHoverLayer' -import RoomHoverLayer from '../../../containers/app/map/layers/RoomHoverLayer' -import { NAVBAR_HEIGHT } from '../../navigation/Navbar' -import { MAP_MOVE_PIXELS_PER_EVENT } from './MapConstants' -import { Provider, useStore } from 'react-redux' - -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), - } - - const moveWithDelta = (deltaX, deltaY) => - setMapPositionWithBoundsCheck(mapPosition.x + deltaX, mapPosition.y + deltaY) - const updateMousePosition = () => { - if (!stage.current) { - return - } - - 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) - - // We explicitly do not specify any dependencies to prevent infinitely dispatching updateDimensions commands - useEffect(() => { - updateDimensions() - - window.addEventListener('resize', updateDimensions) - window.addEventListener('wheel', updateScale) - - window['exportCanvasToImage'] = () => { - const download = document.createElement('a') - download.href = stage.current.getStage().toDataURL() - download.download = 'opendc-canvas-export-' + Date.now() + '.png' - download.click() - } - - return () => { - window.removeEventListener('resize', updateDimensions) - window.removeEventListener('wheel', updateScale) - } - }, []) // eslint-disable-line react-hooks/exhaustive-deps - - const store = useStore() - - 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> - ) -} - -MapStageComponent.propTypes = { - mapDimensions: PropTypes.shape({ - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }).isRequired, - mapPosition: PropTypes.shape({ - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, - }).isRequired, - setMapDimensions: PropTypes.func, - setMapPositionWithBoundsCheck: PropTypes.func, - zoomInOnPosition: PropTypes.func, -} - -export default MapStageComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/RackContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/RackContainer.js new file mode 100644 index 00000000..3c75d3a7 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/RackContainer.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 React from 'react' +import { useSelector } from 'react-redux' +import RackGroup from '../../../components/app/map/groups/RackGroup' +import { Tile } from '../../../shapes' + +const RackContainer = ({ tile }) => { + const interactionLevel = useSelector((state) => state.interactionLevel) + return <RackGroup interactionLevel={interactionLevel} tile={tile} /> +} + +RackContainer.propTypes = { + tile: Tile, +} + +export default RackContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/RackEnergyFillContainer.js index d22317a5..838aea5a 100644 --- a/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js +++ b/opendc-web/opendc-web-ui/src/components/app/map/RackEnergyFillContainer.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' import { useSelector } from 'react-redux' import RackFillBar from '../../../components/app/map/elements/RackFillBar' @@ -29,4 +30,8 @@ const RackSpaceFillContainer = (props) => { return <RackFillBar {...props} {...state} /> } +RackSpaceFillContainer.propTypes = { + tileId: PropTypes.string.isRequired, +} + export default RackSpaceFillContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js new file mode 100644 index 00000000..6791120e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js @@ -0,0 +1,43 @@ +/* + * 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 PropTypes from 'prop-types' +import { useSelector } from 'react-redux' +import RackFillBar from '../../../components/app/map/elements/RackFillBar' + +const RackSpaceFillContainer = (props) => { + const state = useSelector((state) => { + const machineIds = state.objects.rack[state.objects.tile[props.tileId].rack].machines + return { + type: 'space', + fillFraction: machineIds.filter((id) => id !== null).length / machineIds.length, + } + }) + return <RackFillBar {...props} {...state} /> +} + +RackSpaceFillContainer.propTypes = { + tileId: PropTypes.string.isRequired, +} + +export default RackSpaceFillContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/map/RoomContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/RoomContainer.js new file mode 100644 index 00000000..26fbcd7a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/RoomContainer.js @@ -0,0 +1,45 @@ +/* + * 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 from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { goFromBuildingToRoom } from '../../../redux/actions/interaction-level' +import RoomGroup from '../../../components/app/map/groups/RoomGroup' + +const RoomContainer = (props) => { + const state = useSelector((state) => { + return { + interactionLevel: state.interactionLevel, + currentRoomInConstruction: state.construction.currentRoomInConstruction, + room: state.objects.room[props.roomId], + } + }) + const dispatch = useDispatch() + return <RoomGroup {...props} {...state} onClick={() => dispatch(goFromBuildingToRoom(props.roomId))} /> +} + +RoomContainer.propTypes = { + roomId: PropTypes.string, +} + +export default RoomContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/map/TileContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/TileContainer.js new file mode 100644 index 00000000..bfcbf735 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/TileContainer.js @@ -0,0 +1,46 @@ +/* + * 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 PropTypes from 'prop-types' +import { useDispatch, useSelector } from 'react-redux' +import { goFromRoomToRack } from '../../../redux/actions/interaction-level' +import TileGroup from '../../../components/app/map/groups/TileGroup' + +const TileContainer = (props) => { + const interactionLevel = useSelector((state) => state.interactionLevel) + const tile = useSelector((state) => state.objects.tile[props.tileId]) + + const dispatch = useDispatch() + const onClick = (tile) => { + if (tile.rack) { + dispatch(goFromRoomToRack(tile._id)) + } + } + return <TileGroup {...props} onClick={onClick} tile={tile} interactionLevel={interactionLevel} /> +} + +TileContainer.propTypes = { + tileId: PropTypes.string.isRequired, +} + +export default TileContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/map/TopologyContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/TopologyContainer.js new file mode 100644 index 00000000..78e75d0f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/TopologyContainer.js @@ -0,0 +1,35 @@ +/* + * 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 { useSelector } from 'react-redux' +import TopologyGroup from '../../../components/app/map/groups/TopologyGroup' +import { useActiveTopology } from '../../../data/topology' + +const TopologyContainer = () => { + const topology = useActiveTopology() + const interactionLevel = useSelector((state) => state.interactionLevel) + + return <TopologyGroup topology={topology} interactionLevel={interactionLevel} /> +} + +export default TopologyContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/map/WallContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/WallContainer.js new file mode 100644 index 00000000..51dffe4b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/WallContainer.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 PropTypes from 'prop-types' +import { useSelector } from 'react-redux' +import WallGroup from '../../../components/app/map/groups/WallGroup' + +const WallContainer = (props) => { + const tiles = useSelector((state) => + state.objects.room[props.roomId].tiles.map((tileId) => state.objects.tile[tileId]) + ) + return <WallGroup {...props} tiles={tiles} /> +} + +WallContainer.propTypes = { + roomId: PropTypes.string.isRequired, +} + +export default WallContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.js new file mode 100644 index 00000000..f54b7c84 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.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 PropTypes from 'prop-types' +import { ChevronLeftIcon } from '@patternfly/react-icons' +import { collapseContainer } from './Collapse.module.scss' +import { Button } from '@patternfly/react-core' + +function Collapse({ onClick }) { + return ( + <div className={collapseContainer}> + <Button variant="tertiary" onClick={onClick}> + <ChevronLeftIcon /> + </Button> + </div> + ) +} + +Collapse.propTypes = { + onClick: PropTypes.func, +} + +export default Collapse diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.module.scss b/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.module.scss new file mode 100644 index 00000000..0c1fac94 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.module.scss @@ -0,0 +1,55 @@ +/*! + * 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. + */ + +.collapseContainer { + position: absolute; + right: var(--pf-global--spacer--xs); + top: 0; + bottom: 10%; + margin: auto 0; + height: 50px; + + button:global(.pf-m-tertiary) { + height: 100%; + padding: 2px; + + margin-right: var(--pf-global--spacer--xs); + margin-top: var(--pf-global--spacer--xs); + background-color: var(--pf-global--BackgroundColor--100); + border: none; + border-radius: var(--pf-global--BorderRadius--sm); + box-shadow: var(--pf-global--BoxShadow--sm); + + &:not(:global(.pf-m-disabled)) { + background-color: var(--pf-global--BackgroundColor--100); + } + + &:after { + display: none; + } + + &:hover { + border: none; + box-shadow: var(--pf-global--BoxShadow--md); + } + } +} diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js deleted file mode 100644 index 9e8cb36a..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faCamera } from '@fortawesome/free-solid-svg-icons' - -const ExportCanvasComponent = () => ( - <button - className="btn btn-success btn-circle btn-sm" - title="Export Canvas to PNG Image" - onClick={() => window['exportCanvasToImage']()} - > - <FontAwesomeIcon icon={faCamera} /> - </button> -) - -export default ExportCanvasComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicator.js index ef633764..11c2f2d3 100644 --- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicator.js @@ -1,16 +1,16 @@ import PropTypes from 'prop-types' import React from 'react' import { TILE_SIZE_IN_METERS, TILE_SIZE_IN_PIXELS } from '../MapConstants' -import { scaleIndicator } from './ScaleIndicatorComponent.module.scss' +import { scaleIndicator } from './ScaleIndicator.module.scss' -const ScaleIndicatorComponent = ({ scale }) => ( +const ScaleIndicator = ({ scale }) => ( <div className={scaleIndicator} style={{ width: TILE_SIZE_IN_PIXELS * scale }}> {TILE_SIZE_IN_METERS}m </div> ) -ScaleIndicatorComponent.propTypes = { +ScaleIndicator.propTypes = { scale: PropTypes.number.isRequired, } -export default ScaleIndicatorComponent +export default ScaleIndicator diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.module.scss b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicator.module.scss index f19e0ff2..f19e0ff2 100644 --- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.module.scss +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicator.module.scss diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js deleted file mode 100644 index d2f70953..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import ZoomControlContainer from '../../../../containers/app/map/controls/ZoomControlContainer' -import ExportCanvasComponent from './ExportCanvasComponent' -import { toolPanel } from './ToolPanelComponent.module.scss' - -const ToolPanelComponent = () => ( - <div className={toolPanel}> - <ZoomControlContainer /> - <ExportCanvasComponent /> - </div> -) - -export default ToolPanelComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.module.scss b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.module.scss deleted file mode 100644 index 970b1ce2..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.module.scss +++ /dev/null @@ -1,6 +0,0 @@ -.toolPanel { - position: absolute; - left: 10px; - bottom: 10px; - z-index: 50; -} diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.js new file mode 100644 index 00000000..4c60bfb2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { control, toolBar } from './Toolbar.module.scss' +import { Button } from '@patternfly/react-core' +import { SearchPlusIcon, SearchMinusIcon } from '@patternfly/react-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCamera } from '@fortawesome/free-solid-svg-icons' + +const Toolbar = ({ onZoom, onExport }) => ( + <div className={toolBar}> + <Button variant="tertiary" title="Zoom in" onClick={() => onZoom(true)} className={control}> + <SearchPlusIcon /> + </Button> + <Button variant="tertiary" title="Zoom out" onClick={() => onZoom(false)} className={control}> + <SearchMinusIcon /> + </Button> + <Button variant="tertiary" title="Export Canvas to PNG Image" onClick={() => onExport()} className={control}> + <FontAwesomeIcon icon={faCamera} /> + </Button> + </div> +) + +Toolbar.propTypes = { + onZoom: PropTypes.func, + onExport: PropTypes.func, +} + +export default Toolbar diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.module.scss b/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.module.scss new file mode 100644 index 00000000..0d505acc --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.module.scss @@ -0,0 +1,29 @@ +.toolBar { + position: absolute; + bottom: var(--pf-global--spacer--md); + left: var(--pf-global--spacer--xl); +} + +.control { + &:global(.pf-m-tertiary) { + margin-right: var(--pf-global--spacer--xs); + margin-top: var(--pf-global--spacer--xs); + background-color: var(--pf-global--BackgroundColor--100); + border: none; + border-radius: var(--pf-global--BorderRadius--sm); + box-shadow: var(--pf-global--BoxShadow--sm); + + &:not(:global(.pf-m-disabled)) { + background-color: var(--pf-global--BackgroundColor--100); + } + + &:after { + display: none; + } + + &:hover { + border: none; + box-shadow: var(--pf-global--BoxShadow--md); + } + } +} diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js deleted file mode 100644 index 6c3c6cb7..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPlus, faMinus } from '@fortawesome/free-solid-svg-icons' - -const ZoomControlComponent = ({ zoomInOnCenter }) => { - return ( - <span> - <button - className="btn btn-default btn-circle btn-sm mr-1" - title="Zoom in" - onClick={() => zoomInOnCenter(true)} - > - <FontAwesomeIcon icon={faPlus} /> - </button> - <button - className="btn btn-default btn-circle btn-sm mr-1" - title="Zoom out" - onClick={() => zoomInOnCenter(false)} - > - <FontAwesomeIcon icon={faMinus} /> - </button> - </span> - ) -} - -ZoomControlComponent.propTypes = { - zoomInOnCenter: PropTypes.func.isRequired, -} - -export default ZoomControlComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js index 40e28f01..9c4abc4a 100644 --- a/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js @@ -1,10 +1,10 @@ import React from 'react' import { Group } from 'react-konva' -import RackEnergyFillContainer from '../../../../containers/app/map/RackEnergyFillContainer' -import RackSpaceFillContainer from '../../../../containers/app/map/RackSpaceFillContainer' import { Tile } from '../../../../shapes' import { RACK_BACKGROUND_COLOR } from '../../../../util/colors' import TileObject from '../elements/TileObject' +import RackSpaceFillContainer from '../RackSpaceFillContainer' +import RackEnergyFillContainer from '../RackEnergyFillContainer' const RackGroup = ({ tile }) => { return ( diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js index 42d20ff1..a14f3676 100644 --- a/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js @@ -1,10 +1,10 @@ import PropTypes from 'prop-types' import React from 'react' import { Group } from 'react-konva' -import GrayContainer from '../../../../containers/app/map/GrayContainer' -import TileContainer from '../../../../containers/app/map/TileContainer' -import WallContainer from '../../../../containers/app/map/WallContainer' import { InteractionLevel, Room } from '../../../../shapes' +import GrayContainer from '../GrayContainer' +import TileContainer from '../TileContainer' +import WallContainer from '../WallContainer' const RoomGroup = ({ room, interactionLevel, currentRoomInConstruction, onClick }) => { if (currentRoomInConstruction === room._id) { diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js index ce5e4a6b..cd36c7e5 100644 --- a/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js @@ -1,10 +1,10 @@ import PropTypes from 'prop-types' import React from 'react' import { Group } from 'react-konva' -import RackContainer from '../../../../containers/app/map/RackContainer' import { Tile } from '../../../../shapes' import { ROOM_DEFAULT_COLOR, ROOM_IN_CONSTRUCTION_COLOR } from '../../../../util/colors' import RoomTile from '../elements/RoomTile' +import RackContainer from '../RackContainer' const TileGroup = ({ tile, newTile, onClick }) => { let tileObject diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js index d4c6db7d..d3bcb279 100644 --- a/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js @@ -1,8 +1,8 @@ import React from 'react' import { Group } from 'react-konva' -import GrayContainer from '../../../../containers/app/map/GrayContainer' -import RoomContainer from '../../../../containers/app/map/RoomContainer' import { InteractionLevel, Topology } from '../../../../shapes' +import RoomContainer from '../RoomContainer' +import GrayContainer from '../GrayContainer' const TopologyGroup = ({ topology, interactionLevel }) => { if (!topology) { diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js new file mode 100644 index 00000000..badb9f68 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js @@ -0,0 +1,33 @@ +/* + * 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 MapLayerComponent from '../../../../components/app/map/layers/MapLayerComponent' +import { useMapPosition, useMapScale } from '../../../../data/map' + +const MapLayer = (props) => { + const position = useMapPosition() + const scale = useMapScale() + return <MapLayerComponent {...props} mapPosition={position} mapScale={scale} /> +} + +export default MapLayer diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js index 96e6867c..efe5b4e5 100644 --- a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js @@ -1,9 +1,9 @@ import PropTypes from 'prop-types' import React from 'react' import { Group, Layer } from 'react-konva' -import TopologyContainer from '../../../../containers/app/map/TopologyContainer' import Backdrop from '../elements/Backdrop' import GridGroup from '../groups/GridGroup' +import TopologyContainer from '../TopologyContainer' const MapLayerComponent = ({ mapPosition, mapScale }) => ( <Layer> diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayer.js index e9a64545..9a087bd5 100644 --- a/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayer.js @@ -1,3 +1,25 @@ +/* + * 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 { useDispatch, useSelector } from 'react-redux' import { addRackToTile } from '../../../../redux/actions/topology/room' diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayer.js index 4070c766..87240813 100644 --- a/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayer.js @@ -1,3 +1,25 @@ +/* + * 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 { useDispatch, useSelector } from 'react-redux' import { toggleTileAtLocation } from '../../../../redux/actions/topology/building' diff --git a/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultInfo.js b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultInfo.js new file mode 100644 index 00000000..09348e60 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultInfo.js @@ -0,0 +1,40 @@ +/* + * 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 { Tooltip } from '@patternfly/react-core' +import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons' +import { METRIC_DESCRIPTIONS } from '../../../util/available-metrics' + +function PortfolioResultInfo({ metric }) { + return ( + <Tooltip position="top" content={<div>{METRIC_DESCRIPTIONS[metric]}</div>}> + <OutlinedQuestionCircleIcon title="Metric information" /> + </Tooltip> + ) +} + +PortfolioResultInfo.propTypes = { + metric: PropTypes.string.isRequired, +} + +export default PortfolioResultInfo diff --git a/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResults.js b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResults.js new file mode 100644 index 00000000..6a96c70c --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResults.js @@ -0,0 +1,134 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Bar, CartesianGrid, ComposedChart, ErrorBar, ResponsiveContainer, Scatter, XAxis, YAxis } from 'recharts' +import { AVAILABLE_METRICS, METRIC_NAMES, METRIC_NAMES_SHORT, METRIC_UNITS } from '../../../util/available-metrics' +import { mean, std } from 'mathjs' +import approx from 'approximate-number' +import { + Bullseye, + Card, + CardActions, + CardBody, + CardHeader, + CardTitle, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + Grid, + GridItem, + Spinner, + Title, +} from '@patternfly/react-core' +import { ErrorCircleOIcon, CubesIcon } from '@patternfly/react-icons' +import { usePortfolioScenarios } from '../../../data/project' +import NewScenario from '../../projects/NewScenario' +import PortfolioResultInfo from './PortfolioResultInfo' + +const PortfolioResults = ({ portfolioId }) => { + const { status, data: scenarios = [] } = usePortfolioScenarios(portfolioId) + + if (status === 'loading') { + return ( + <Bullseye> + <EmptyState> + <EmptyStateIcon variant="container" component={Spinner} /> + <Title size="lg" headingLevel="h4"> + Loading Results + </Title> + </EmptyState> + </Bullseye> + ) + } else if (status === 'error') { + return ( + <Bullseye> + <EmptyState> + <EmptyStateIcon variant="container" component={ErrorCircleOIcon} /> + <Title size="lg" headingLevel="h4"> + Unable to connect + </Title> + <EmptyStateBody> + There was an error retrieving data. Check your connection and try again. + </EmptyStateBody> + </EmptyState> + </Bullseye> + ) + } else if (scenarios.length === 0) { + return ( + <Bullseye> + <EmptyState> + <EmptyStateIcon variant="container" component={CubesIcon} /> + <Title size="lg" headingLevel="h4"> + No results + </Title> + <EmptyStateBody> + No results are currently available for this portfolio. Run a scenario to obtain simulation + results. + </EmptyStateBody> + <NewScenario portfolioId={portfolioId} /> + </EmptyState> + </Bullseye> + ) + } + + const dataPerMetric = {} + + AVAILABLE_METRICS.forEach((metric) => { + dataPerMetric[metric] = scenarios + .filter((scenario) => scenario.results) + .map((scenario) => ({ + name: scenario.name, + value: mean(scenario.results[metric]), + errorX: std(scenario.results[metric]), + })) + }) + + return ( + <Grid hasGutter> + {AVAILABLE_METRICS.map((metric) => ( + <GridItem xl={6} lg={12} key={metric}> + <Card> + <CardHeader> + <CardActions> + <PortfolioResultInfo metric={metric} /> + </CardActions> + <CardTitle>{METRIC_NAMES[metric]}</CardTitle> + </CardHeader> + <CardBody> + <ResponsiveContainer aspect={16 / 9} width="100%"> + <ComposedChart + data={dataPerMetric[metric]} + margin={{ left: 35, bottom: 15 }} + layout="vertical" + > + <CartesianGrid strokeDasharray="3 3" /> + <XAxis + tickFormatter={(tick) => approx(tick)} + label={{ value: METRIC_UNITS[metric], position: 'bottom', offset: 0 }} + type="number" + /> + <YAxis dataKey="name" type="category" /> + <Bar dataKey="value" fill="#3399FF" isAnimationActive={false} /> + <Scatter dataKey="value" opacity={0} isAnimationActive={false}> + <ErrorBar + dataKey="errorX" + width={10} + strokeWidth={3} + stroke="#FF6600" + direction="x" + /> + </Scatter> + </ComposedChart> + </ResponsiveContainer> + </CardBody> + </Card> + </GridItem> + ))} + </Grid> + ) +} + +PortfolioResults.propTypes = { + portfolioId: PropTypes.string, +} + +export default PortfolioResults diff --git a/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js deleted file mode 100644 index 983a5c1d..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { Bar, CartesianGrid, ComposedChart, ErrorBar, ResponsiveContainer, Scatter, XAxis, YAxis } from 'recharts' -import { AVAILABLE_METRICS, METRIC_NAMES_SHORT, METRIC_UNITS } from '../../../util/available-metrics' -import { mean, std } from 'mathjs' -import { Portfolio, Scenario } from '../../../shapes' -import approx from 'approximate-number' - -const PortfolioResultsComponent = ({ portfolio, scenarios }) => { - if (!portfolio) { - return <div>Loading...</div> - } - - const nonFinishedScenarios = scenarios.filter((s) => s.simulation.state !== 'FINISHED') - - if (nonFinishedScenarios.length > 0) { - if (nonFinishedScenarios.every((s) => s.simulation.state === 'QUEUED' || s.simulation.state === 'RUNNING')) { - return ( - <div> - <h1>Simulation running...</h1> - <p>{nonFinishedScenarios.length} of the scenarios are still being simulated</p> - </div> - ) - } - if (nonFinishedScenarios.some((s) => s.simulation.state === 'FAILED')) { - return ( - <div> - <h1>Simulation failed.</h1> - <p> - Try again by creating a new scenario. Please contact the OpenDC team for support, if issues - persist. - </p> - </div> - ) - } - } - - const dataPerMetric = {} - - AVAILABLE_METRICS.forEach((metric) => { - dataPerMetric[metric] = scenarios.map((scenario) => ({ - name: scenario.name, - value: mean(scenario.results[metric]), - errorX: std(scenario.results[metric]), - })) - }) - - return ( - <div className="full-height" style={{ overflowY: 'scroll', overflowX: 'hidden' }}> - <h2>Portfolio: {portfolio.name}</h2> - <p>Repeats per Scenario: {portfolio.targets.repeatsPerScenario}</p> - <div className="row"> - {AVAILABLE_METRICS.map((metric) => ( - <div className="col-6 mb-2" key={metric}> - <h4>{METRIC_NAMES_SHORT[metric]}</h4> - <ResponsiveContainer aspect={16 / 9} width="100%"> - <ComposedChart - data={dataPerMetric[metric]} - margin={{ left: 35, bottom: 15 }} - layout="vertical" - > - <CartesianGrid strokeDasharray="3 3" /> - <XAxis - tickFormatter={(tick) => approx(tick)} - label={{ value: METRIC_UNITS[metric], position: 'bottom', offset: 0 }} - type="number" - /> - <YAxis dataKey="name" type="category" /> - <Bar dataKey="value" fill="#3399FF" isAnimationActive={false} /> - <Scatter dataKey="value" opacity={0} isAnimationActive={false}> - <ErrorBar - dataKey="errorX" - width={10} - strokeWidth={3} - stroke="#FF6600" - direction="x" - /> - </Scatter> - </ComposedChart> - </ResponsiveContainer> - </div> - ))} - </div> - </div> - ) -} - -PortfolioResultsComponent.propTypes = { - portfolio: Portfolio, - scenarios: PropTypes.arrayOf(Scenario), -} - -export default PortfolioResultsComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js deleted file mode 100644 index 56fa799f..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types' -import classNames from 'classnames' -import React, { useState } from 'react' -import { collapseButton, collapseButtonRight, sidebar, sidebarRight } from './Sidebar.module.scss' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons' - -function Sidebar({ isRight, collapsible = true, children }) { - const [isCollapsed, setCollapsed] = useState(false) - - const button = ( - <div - className={classNames(collapseButton, { - [collapseButtonRight]: isRight, - })} - onClick={() => setCollapsed(!isCollapsed)} - > - {(isCollapsed && isRight) || (!isCollapsed && !isRight) ? ( - <FontAwesomeIcon icon={faAngleLeft} title={isRight ? 'Expand' : 'Collapse'} /> - ) : ( - <FontAwesomeIcon icon={faAngleRight} title={isRight ? 'Collapse' : 'Expand'} /> - )} - </div> - ) - - if (isCollapsed) { - return button - } - return ( - <div - className={classNames(`${sidebar} p-3 h-100`, { - [sidebarRight]: isRight, - })} - onWheel={(e) => e.stopPropagation()} - > - {children} - {collapsible && button} - </div> - ) -} - -Sidebar.propTypes = { - isRight: PropTypes.bool.isRequired, - collapsible: PropTypes.bool, - children: PropTypes.node, -} - -export default Sidebar diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.module.scss b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.module.scss deleted file mode 100644 index 19c6a97f..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.module.scss +++ /dev/null @@ -1,57 +0,0 @@ -@import 'src/style/_variables.scss'; -@import 'src/style/_mixins.scss'; - -.collapseButton { - position: absolute; - left: 5px; - top: 5px; - padding: 5px 7px; - - background: white; - border: solid 1px $gray-semi-light; - z-index: 99; - - @include clickable; - border-radius: 5px; - transition: background 200ms; - - &.collapseButtonRight { - left: auto; - right: 5px; - top: 5px; - } - - &:hover { - background: #eeeeee; - } -} - -.sidebar { - position: absolute; - top: 0; - left: 0; - width: $side-bar-width; - - z-index: 100; - background: white; - - border-right: $gray-semi-dark 1px solid; - - .collapseButton { - left: auto; - right: -25px; - } -} - -.sidebarRight { - left: auto; - right: 0; - - border-left: $gray-semi-dark 1px solid; - border-right: none; - - .collapseButtonRight { - left: -25px; - right: auto; - } -} 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 deleted file mode 100644 index d61ff24e..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js +++ /dev/null @@ -1,71 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { Portfolio } from '../../../../shapes' -import Link from 'next/link' -import ScenarioListContainer from '../../../../containers/app/sidebars/project/ScenarioListContainer' -import { Button, Col, Row } from 'reactstrap' -import classNames from 'classnames' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPlus, faPlay, faTrash } from '@fortawesome/free-solid-svg-icons' - -function PortfolioListComponent({ - portfolios, - currentProjectId, - currentPortfolioId, - onNewPortfolio, - onChoosePortfolio, - onDeletePortfolio, -}) { - return ( - <div className="pb-3"> - <h2> - Portfolios - <Button color="primary" outline className="float-right" onClick={(e) => onNewPortfolio(e)}> - <FontAwesomeIcon icon={faPlus} /> - </Button> - </h2> - - {portfolios.map((portfolio) => ( - <div key={portfolio._id}> - <Row className="row mb-1"> - <Col - xs="7" - className={classNames('align-self-center', { - 'font-weight-bold': portfolio._id === currentPortfolioId, - })} - > - {portfolio.name} - </Col> - <Col xs="5" className="text-right"> - <Link passHref href={`/projects/${currentProjectId}/portfolios/${portfolio._id}`}> - <Button - color="primary" - outline - className="mr-1" - onClick={() => onChoosePortfolio(portfolio._id)} - > - <FontAwesomeIcon icon={faPlay} /> - </Button> - </Link> - <Button color="danger" outline onClick={() => onDeletePortfolio(portfolio._id)}> - <FontAwesomeIcon icon={faTrash} /> - </Button> - </Col> - </Row> - <ScenarioListContainer portfolioId={portfolio._id} /> - </div> - ))} - </div> - ) -} - -PortfolioListComponent.propTypes = { - portfolios: PropTypes.arrayOf(Portfolio), - currentProjectId: PropTypes.string, - currentPortfolioId: PropTypes.string, - onNewPortfolio: PropTypes.func.isRequired, - onChoosePortfolio: PropTypes.func.isRequired, - onDeletePortfolio: PropTypes.func.isRequired, -} - -export default PortfolioListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js deleted file mode 100644 index 10d22e5b..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import Sidebar from '../Sidebar' -import TopologyListContainer from '../../../../containers/app/sidebars/project/TopologyListContainer' -import PortfolioListContainer from '../../../../containers/app/sidebars/project/PortfolioListContainer' -import { Container } from 'reactstrap' - -const ProjectSidebarComponent = ({ collapsible }) => ( - <Sidebar isRight={false} collapsible={collapsible}> - <Container fluid className="h-100 overflow-auto"> - <TopologyListContainer /> - <PortfolioListContainer /> - </Container> - </Sidebar> -) - -ProjectSidebarComponent.propTypes = { - collapsible: PropTypes.bool, -} - -export default ProjectSidebarComponent 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 deleted file mode 100644 index e81d2b78..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js +++ /dev/null @@ -1,45 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { Scenario } from '../../../../shapes' -import { Button, Col, Row } from 'reactstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons' - -function ScenarioListComponent({ scenarios, portfolioId, onNewScenario, onDeleteScenario }) { - return ( - <> - {scenarios.map((scenario, idx) => ( - <Row key={scenario._id} className="mb-1"> - <Col xs="7" className="pl-5 align-self-center"> - {scenario.name} - </Col> - <Col xs="5" className="text-right"> - <Button - color="danger" - outline - disabled={idx === 0} - onClick={() => (idx !== 0 ? onDeleteScenario(scenario._id) : undefined)} - > - <FontAwesomeIcon icon={faTrash} /> - </Button> - </Col> - </Row> - ))} - <div className="pl-4 mb-2"> - <Button color="primary" outline onClick={() => onNewScenario(portfolioId)}> - <FontAwesomeIcon icon={faPlus} className="mr-1" /> - New scenario - </Button> - </div> - </> - ) -} - -ScenarioListComponent.propTypes = { - scenarios: PropTypes.arrayOf(Scenario), - portfolioId: PropTypes.string, - onNewScenario: PropTypes.func.isRequired, - onDeleteScenario: PropTypes.func.isRequired, -} - -export default ScenarioListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js deleted file mode 100644 index ac58669b..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js +++ /dev/null @@ -1,56 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { Topology } from '../../../../shapes' -import { Button, Col, Row } from 'reactstrap' -import classNames from 'classnames' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPlus, faPlay, faTrash } from '@fortawesome/free-solid-svg-icons' - -function TopologyListComponent({ topologies, currentTopologyId, onChooseTopology, onNewTopology, onDeleteTopology }) { - return ( - <div className="pb-3"> - <h2> - Topologies - <Button color="primary" outline className="float-right" onClick={onNewTopology}> - <FontAwesomeIcon icon={faPlus} /> - </Button> - </h2> - - {topologies.map((topology, idx) => ( - <Row key={topology._id} className="mb-1"> - <Col - xs="7" - className={classNames('align-self-center', { - 'font-weight-bold': topology._id === currentTopologyId, - })} - > - {topology.name} - </Col> - <Col xs="5" className="text-right"> - <Button color="primary" outline className="mr-1" onClick={() => onChooseTopology(topology._id)}> - <FontAwesomeIcon icon={faPlay} /> - </Button> - <Button - color="danger" - outline - disabled={idx === 0} - onClick={() => (idx !== 0 ? onDeleteTopology(topology._id) : undefined)} - > - <FontAwesomeIcon icon={faTrash} /> - </Button> - </Col> - </Row> - ))} - </div> - ) -} - -TopologyListComponent.propTypes = { - topologies: PropTypes.arrayOf(Topology), - currentTopologyId: PropTypes.string, - onChooseTopology: PropTypes.func.isRequired, - onNewTopology: PropTypes.func.isRequired, - onDeleteTopology: PropTypes.func.isRequired, -} - -export default TopologyListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js index b8c88003..ececd07b 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js @@ -1,16 +1,65 @@ import PropTypes from 'prop-types' -import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPencilAlt } from '@fortawesome/free-solid-svg-icons' - -const NameComponent = ({ name, onEdit }) => ( - <h2> - {name} - <button className="btn btn-outline-secondary float-right" onClick={onEdit}> - <FontAwesomeIcon icon={faPencilAlt} /> - </button> - </h2> -) +import React, { useRef, useState } from 'react' +import { Button, TextInput } from '@patternfly/react-core' +import { PencilAltIcon, CheckIcon, TimesIcon } from '@patternfly/react-icons' + +function NameComponent({ name, onEdit }) { + const [isEditing, setEditing] = useState(false) + const nameInput = useRef(null) + + const onCancel = () => { + nameInput.current.value = name + setEditing(false) + } + + const onSubmit = (event) => { + if (event) { + event.preventDefault() + } + + const name = nameInput.current.value + if (name) { + onEdit(name) + } + + setEditing(false) + } + + return ( + <form + className={`pf-c-inline-edit ${isEditing ? 'pf-m-inline-editable' : ''} pf-u-display-inline-block`} + onSubmit={onSubmit} + > + <div className="pf-c-inline-edit__group"> + <div className="pf-c-inline-edit__value" id="single-inline-edit-example-label"> + {name} + </div> + <div className="pf-c-inline-edit__action pf-m-enable-editable"> + <Button className="pf-u-py-0" variant="plain" aria-label="Edit" onClick={() => setEditing(true)}> + <PencilAltIcon /> + </Button> + </div> + </div> + <div className="pf-c-inline-edit__group"> + <div className="pf-c-inline-edit__input"> + <TextInput type="text" defaultValue={name} ref={nameInput} aria-label="Editable text input" /> + </div> + <div className="pf-c-inline-edit__group pf-m-action-group pf-m-icon-group"> + <div className="pf-c-inline-edit__action pf-m-valid"> + <Button className="pf-u-py-0" variant="plain" aria-label="Save edits" onClick={onSubmit}> + <CheckIcon /> + </Button> + </div> + <div className="pf-c-inline-edit__action"> + <Button className="pf-u-py-0" variant="plain" aria-label="Cancel edits" onClick={onCancel}> + <TimesIcon /> + </Button> + </div> + </div> + </div> + </form> + ) +} NameComponent.propTypes = { name: PropTypes.string, diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.js new file mode 100644 index 00000000..c4a880b1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { InteractionLevel } from '../../../../shapes' +import BuildingSidebarComponent from './building/BuildingSidebarComponent' +import { + Button, + DrawerActions, + DrawerCloseButton, + DrawerHead, + DrawerPanelBody, + DrawerPanelContent, + Flex, + Title, +} from '@patternfly/react-core' +import { AngleLeftIcon } from '@patternfly/react-icons' +import { useDispatch } from 'react-redux' +import { goDownOneInteractionLevel } from '../../../../redux/actions/interaction-level' +import { backButton } from './TopologySidebar.module.scss' +import RoomSidebarContainer from './room/RoomSidebarContainer' +import RackSidebarContainer from './rack/RackSidebarContainer' +import MachineSidebarContainer from './machine/MachineSidebarContainer' + +const name = { + BUILDING: 'Building', + ROOM: 'Room', + RACK: 'Rack', + MACHINE: 'Machine', +} + +const TopologySidebar = ({ interactionLevel, onClose }) => { + let sidebarContent + + switch (interactionLevel.mode) { + case 'BUILDING': + sidebarContent = <BuildingSidebarComponent /> + break + case 'ROOM': + sidebarContent = <RoomSidebarContainer /> + break + case 'RACK': + sidebarContent = <RackSidebarContainer /> + break + case 'MACHINE': + sidebarContent = <MachineSidebarContainer /> + break + default: + sidebarContent = 'Missing Content' + } + + const dispatch = useDispatch() + const onClick = () => dispatch(goDownOneInteractionLevel()) + + return ( + <DrawerPanelContent isResizable defaultSize="450px" minSize="400px"> + <DrawerHead> + <Flex> + <Button + variant="tertiary" + isSmall + className={backButton} + onClick={interactionLevel.mode === 'BUILDING' ? onClose : onClick} + > + <AngleLeftIcon /> + </Button> + <Title className="pf-u-align-self-center" headingLevel="h1"> + {name[interactionLevel.mode]} + </Title> + </Flex> + <DrawerActions> + <DrawerCloseButton onClose={onClose} /> + </DrawerActions> + </DrawerHead> + <DrawerPanelBody>{sidebarContent}</DrawerPanelBody> + </DrawerPanelContent> + ) +} + +TopologySidebar.propTypes = { + interactionLevel: InteractionLevel, + onClose: PropTypes.func, +} + +export default TopologySidebar diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.module.scss b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.module.scss new file mode 100644 index 00000000..45dc98da --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.module.scss @@ -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. + */ + +.backButton { + &:global(.pf-c-button) { + align-self: center; + --pf-c-button--after--BorderColor: var(--pf-global--BorderColor--light-100); + color: var(--pf-global--Color--400); + + --pf-c-button--PaddingRight: var(--pf-global--spacer--sm); + --pf-c-button--PaddingLeft: var(--pf-global--spacer--sm); + + &:hover, + &:focus { + --pf-c-button--after--BorderColor: var(--pf-global--BorderColor--100); + } + } +} diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js deleted file mode 100644 index 450df6cd..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react' -import BuildingSidebarContainer from '../../../../containers/app/sidebars/topology/building/BuildingSidebarContainer' -import MachineSidebarContainer from '../../../../containers/app/sidebars/topology/machine/MachineSidebarContainer' -import RackSidebarContainer from '../../../../containers/app/sidebars/topology/rack/RackSidebarContainer' -import RoomSidebarContainer from '../../../../containers/app/sidebars/topology/room/RoomSidebarContainer' -import Sidebar from '../Sidebar' -import { InteractionLevel } from '../../../../shapes' - -const TopologySidebarComponent = ({ interactionLevel }) => { - let sidebarContent - - switch (interactionLevel.mode) { - case 'BUILDING': - sidebarContent = <BuildingSidebarContainer /> - break - case 'ROOM': - sidebarContent = <RoomSidebarContainer /> - break - case 'RACK': - sidebarContent = <RackSidebarContainer /> - break - case 'MACHINE': - sidebarContent = <MachineSidebarContainer /> - break - default: - sidebarContent = 'Missing Content' - } - - return <Sidebar isRight={true}>{sidebarContent}</Sidebar> -} - -TopologySidebarComponent.propTypes = { - interactionLevel: InteractionLevel, -} - -export default TopologySidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js index eea62f84..6c2556d3 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js @@ -1,13 +1,8 @@ import React from 'react' -import NewRoomConstructionContainer from '../../../../../containers/app/sidebars/topology/building/NewRoomConstructionContainer' +import NewRoomConstructionContainer from './NewRoomConstructionContainer' const BuildingSidebarComponent = () => { - return ( - <div> - <h2>Building</h2> - <NewRoomConstructionContainer /> - </div> - ) + return <NewRoomConstructionContainer /> } export default BuildingSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js index e8c81735..656b2515 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js @@ -1,29 +1,38 @@ import PropTypes from 'prop-types' import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPlus, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons' -import { Button } from 'reactstrap' +import { Button, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core' +import PlusIcon from '@patternfly/react-icons/dist/js/icons/plus-icon' +import CheckIcon from '@patternfly/react-icons/dist/js/icons/check-icon' const NewRoomConstructionComponent = ({ onStart, onFinish, onCancel, currentRoomInConstruction }) => { if (currentRoomInConstruction === '-1') { return ( - <div className="btn btn-outline-primary btn-block" onClick={onStart}> - <FontAwesomeIcon icon={faPlus} className="mr-2" /> + <Button isBlock icon={<PlusIcon />} onClick={onStart}> Construct a new room - </div> + </Button> ) } return ( - <div> - <Button color="primary" block onClick={onFinish}> - <FontAwesomeIcon icon={faCheck} className="mr-2" /> - Finalize new room - </Button> - <Button color="default" block onClick={onCancel}> - <FontAwesomeIcon icon={faTimes} className="mr-2" /> - Cancel construction - </Button> - </div> + <Toolbar + inset={{ + default: 'insetNone', + }} + > + <ToolbarContent> + <ToolbarGroup> + <ToolbarItem> + <Button icon={<CheckIcon />} onClick={onFinish}> + Finalize new room + </Button> + </ToolbarItem> + <ToolbarItem widths={{ default: '100%' }}> + <Button isBlock variant="secondary" onClick={onCancel}> + Cancel + </Button> + </ToolbarItem> + </ToolbarGroup> + </ToolbarContent> + </Toolbar> ) } diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionContainer.js new file mode 100644 index 00000000..0836263c --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionContainer.js @@ -0,0 +1,46 @@ +/* + * 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 { useDispatch, useSelector } from 'react-redux' +import { + cancelNewRoomConstruction, + finishNewRoomConstruction, + startNewRoomConstruction, +} from '../../../../../redux/actions/topology/building' +import NewRoomConstructionComponent from './NewRoomConstructionComponent' + +const NewRoomConstructionButton = (props) => { + const currentRoomInConstruction = useSelector((state) => state.construction.currentRoomInConstruction) + + const dispatch = useDispatch() + const actions = { + onStart: () => dispatch(startNewRoomConstruction()), + onFinish: () => dispatch(finishNewRoomConstruction()), + onCancel: () => dispatch(cancelNewRoomConstruction()), + } + return ( + <NewRoomConstructionComponent {...props} {...actions} currentRoomInConstruction={currentRoomInConstruction} /> + ) +} + +export default NewRoomConstructionButton diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js deleted file mode 100644 index 829bf265..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js +++ /dev/null @@ -1,17 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faAngleLeft } from '@fortawesome/free-solid-svg-icons' - -const BackToRackComponent = ({ onClick }) => ( - <div className="btn btn-secondary btn-block" onClick={onClick}> - <FontAwesomeIcon icon={faAngleLeft} className="mr-2" /> - Back to rack - </div> -) - -BackToRackComponent.propTypes = { - onClick: PropTypes.func, -} - -export default BackToRackComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachine.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachine.js new file mode 100644 index 00000000..a7bf3719 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachine.js @@ -0,0 +1,54 @@ +/* + * 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, { useState } from 'react' +import { useDispatch } from 'react-redux' +import { deleteMachine } from '../../../../../redux/actions/topology/machine' +import { Button } from '@patternfly/react-core' +import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon' +import ConfirmationModal from '../../../../modals/ConfirmationModal' + +const DeleteMachine = () => { + const dispatch = useDispatch() + const [isVisible, setVisible] = useState(false) + const callback = (isConfirmed) => { + if (isConfirmed) { + dispatch(deleteMachine()) + } + setVisible(false) + } + return ( + <> + <Button variant="danger" icon={<TrashIcon />} isBlock onClick={() => setVisible(true)}> + Delete this machine + </Button> + <ConfirmationModal + title="Delete this machine" + message="Are you sure you want to delete this machine?" + isOpen={isVisible} + callback={callback} + /> + </> + ) +} + +export default DeleteMachine diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js index 88a99e0f..d3d4a8cf 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js @@ -1,17 +1,38 @@ import PropTypes from 'prop-types' import React from 'react' -import BackToRackContainer from '../../../../../containers/app/sidebars/topology/machine/BackToRackContainer' -import DeleteMachineContainer from '../../../../../containers/app/sidebars/topology/machine/DeleteMachineContainer' -import MachineNameContainer from '../../../../../containers/app/sidebars/topology/machine/MachineNameContainer' -import UnitTabsContainer from '../../../../../containers/app/sidebars/topology/machine/UnitTabsContainer' +import UnitTabsComponent from './UnitTabsComponent' +import DeleteMachine from './DeleteMachine' +import { + TextContent, + TextList, + TextListItem, + TextListItemVariants, + TextListVariants, + Title, +} from '@patternfly/react-core' +import { useSelector } from 'react-redux' const MachineSidebarComponent = ({ machineId }) => { + const machine = useSelector((state) => state.objects.machine[machineId]) return ( - <div className="h-100 overflow-auto"> - <MachineNameContainer /> - <BackToRackContainer /> - <DeleteMachineContainer /> - <UnitTabsContainer /> + <div> + <TextContent> + <Title headingLevel="h2">Details</Title> + <TextList component={TextListVariants.dl}> + <TextListItem component={TextListItemVariants.dt}>Name</TextListItem> + <TextListItem component={TextListItemVariants.dd}> + Machine at position {machine.position} + </TextListItem> + </TextList> + + <Title headingLevel="h2">Actions</Title> + <DeleteMachine /> + + <Title headingLevel="h2">Units</Title> + </TextContent> + <div className="pf-u-h-100"> + <UnitTabsComponent /> + </div> </div> ) } diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarContainer.js new file mode 100644 index 00000000..94d9f2c3 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarContainer.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 React from 'react' +import { useSelector } from 'react-redux' +import MachineSidebarComponent from './MachineSidebarComponent' + +const MachineSidebarContainer = (props) => { + const machineId = useSelector( + (state) => + state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].machines[ + state.interactionLevel.position - 1 + ] + ) + return <MachineSidebarComponent {...props} machineId={machineId} /> +} + +export default MachineSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js index 532add37..88591208 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js @@ -1,28 +1,36 @@ import PropTypes from 'prop-types' -import React, { useRef } from 'react' -import { Button, Form, FormGroup, Input } from 'reactstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPlus } from '@fortawesome/free-solid-svg-icons' +import React, { useState } from 'react' +import { Button, InputGroup, Select, SelectOption, SelectVariant } from '@patternfly/react-core' +import PlusIcon from '@patternfly/react-icons/dist/js/icons/plus-icon' function UnitAddComponent({ units, onAdd }) { - const unitSelect = useRef(null) + const [isOpen, setOpen] = useState(false) + const [selected, setSelected] = useState(null) return ( - <Form inline> - <FormGroup className="w-100"> - <Input type="select" className="w-70 mr-1" innerRef={unitSelect}> - {units.map((unit) => ( - <option value={unit._id} key={unit._id}> - {unit.name} - </option> - ))} - </Input> - <Button color="primary" outline onClick={() => onAdd(unitSelect.current.value)}> - <FontAwesomeIcon icon={faPlus} className="mr-2" /> - Add - </Button> - </FormGroup> - </Form> + <InputGroup> + <Select + variant={SelectVariant.single} + placeholderText="Select a unit" + aria-label="Select Unit" + onToggle={() => setOpen(!isOpen)} + isOpen={isOpen} + onSelect={(_, selection) => { + setSelected(selection) + setOpen(false) + }} + selections={selected} + > + {units.map((unit) => ( + <SelectOption value={unit._id} key={unit._id}> + {unit.name} + </SelectOption> + ))} + </Select> + <Button icon={<PlusIcon />} variant="control" onClick={() => onAdd(selected)}> + Add + </Button> + </InputGroup> ) } diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddContainer.js new file mode 100644 index 00000000..8a6680e6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddContainer.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 PropTypes from 'prop-types' +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { addUnit } from '../../../../../redux/actions/topology/machine' +import UnitAddComponent from './UnitAddComponent' + +const UnitAddContainer = ({ unitType }) => { + const units = useSelector((state) => Object.values(state.objects[unitType])) + const dispatch = useDispatch() + + const onAdd = (id) => dispatch(addUnit(unitType, id)) + + return <UnitAddComponent onAdd={onAdd} units={units} /> +} + +UnitAddContainer.propTypes = { + unitType: PropTypes.string.isRequired, +} + +export default UnitAddContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js deleted file mode 100644 index 46c639bd..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { UncontrolledPopover, PopoverHeader, PopoverBody, Button } from 'reactstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faTrash, faInfoCircle } from '@fortawesome/free-solid-svg-icons' -import { ProcessingUnit, StorageUnit } from '../../../../../shapes' - -function UnitComponent({ index, unitType, unit, onDelete }) { - let unitInfo - if (unitType === 'cpu' || unitType === 'gpu') { - unitInfo = ( - <> - <strong>Clockrate: </strong> - <code>{unit.clockRateMhz}</code> - <br /> - <strong>Num. Cores: </strong> - <code>{unit.numberOfCores}</code> - <br /> - <strong>Energy Cons.: </strong> - <code>{unit.energyConsumptionW} W</code> - <br /> - </> - ) - } else if (unitType === 'memory' || unitType === 'storage') { - unitInfo = ( - <> - <strong>Speed:</strong> - <code>{unit.speedMbPerS} Mb/s</code> - <br /> - <strong>Size:</strong> - <code>{unit.sizeMb} MB</code> - <br /> - <strong>Energy Cons.:</strong> - <code>{unit.energyConsumptionW} W</code> - <br /> - </> - ) - } - - return ( - <li className="d-flex list-group-item justify-content-between align-items-center"> - <span style={{ maxWidth: '60%' }}>{unit.name}</span> - <span> - <Button outline={true} color="info" className="mr-1" id={`unit-${index}`}> - <FontAwesomeIcon icon={faInfoCircle} /> - </Button> - <UncontrolledPopover trigger="focus" placement="left" target={`unit-${index}`}> - <PopoverHeader>Unit Information</PopoverHeader> - <PopoverBody>{unitInfo}</PopoverBody> - </UncontrolledPopover> - - <Button outline color="danger" onClick={onDelete}> - <FontAwesomeIcon icon={faTrash} /> - </Button> - </span> - </li> - ) -} - -UnitComponent.propTypes = { - index: PropTypes.number, - unitType: PropTypes.string, - unit: PropTypes.oneOfType([ProcessingUnit, StorageUnit]), - onDelete: PropTypes.func, -} - -export default UnitComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js index 54c1a6cc..9c3c08fd 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js @@ -1,29 +1,107 @@ import PropTypes from 'prop-types' import React from 'react' import { ProcessingUnit, StorageUnit } from '../../../../../shapes' -import UnitComponent from './UnitComponent' +import { + Button, + DataList, + DataListAction, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + Popover, + Title, +} from '@patternfly/react-core' +import { CubesIcon, InfoIcon, TrashIcon } from '@patternfly/react-icons' -const UnitListComponent = ({ unitType, units, onDelete }) => ( - <ul className="list-group mt-1"> - {units.length !== 0 ? ( - units.map((unit, index) => ( - <UnitComponent - unitType={unitType} - unit={unit} - onDelete={() => onDelete(unit, unitType)} - index={index} - key={index} - /> - )) - ) : ( - <div className="alert alert-info"> - <span> - <strong>No units...</strong> Add some with the menu above! - </span> - </div> - )} - </ul> -) +const UnitInfo = ({ unit, unitType }) => { + if (unitType === 'cpu' || unitType === 'gpu') { + return ( + <DescriptionList> + <DescriptionListGroup> + <DescriptionListTerm>Clock Frequency</DescriptionListTerm> + <DescriptionListDescription>{unit.clockRateMhz} MHz</DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Number of Cores</DescriptionListTerm> + <DescriptionListDescription>{unit.numberOfCores}</DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Energy Consumption</DescriptionListTerm> + <DescriptionListDescription>{unit.energyConsumptionW} W</DescriptionListDescription> + </DescriptionListGroup> + </DescriptionList> + ) + } + + return ( + <DescriptionList> + <DescriptionListGroup> + <DescriptionListTerm>Speed</DescriptionListTerm> + <DescriptionListDescription>{unit.speedMbPerS} Mb/s</DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Capacity</DescriptionListTerm> + <DescriptionListDescription>{unit.sizeMb} MB</DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Energy Consumption</DescriptionListTerm> + <DescriptionListDescription>{unit.energyConsumptionW} W</DescriptionListDescription> + </DescriptionListGroup> + </DescriptionList> + ) +} + +UnitInfo.propTypes = { + unitType: PropTypes.string.isRequired, + unit: PropTypes.oneOfType([ProcessingUnit, StorageUnit]).isRequired, +} + +const UnitListComponent = ({ unitType, units, onDelete }) => { + if (units.length === 0) { + return ( + <EmptyState> + <EmptyStateIcon icon={CubesIcon} /> + <Title headingLevel="h5" size="lg"> + No units found + </Title> + <EmptyStateBody>You have not configured any units yet. Add some with the menu above!</EmptyStateBody> + </EmptyState> + ) + } + + return ( + <DataList aria-label="Machine Units" isCompact> + {units.map((unit, index) => ( + <DataListItem key={index}> + <DataListItemRow> + <DataListItemCells dataListCells={[<DataListCell key="unit">{unit.name}</DataListCell>]} /> + <DataListAction id="goto" aria-label="Goto Machine" aria-labelledby="goto"> + <Popover + headerContent="Unit Information" + bodyContent={<UnitInfo unitType={unitType} unit={unit} />} + > + <Button isSmall variant="plain" className="pf-u-p-0"> + <InfoIcon /> + </Button> + </Popover> + <Button isSmall variant="plain" className="pf-u-p-0" onClick={() => onDelete(units[index])}> + <TrashIcon /> + </Button> + </DataListAction> + </DataListItemRow> + </DataListItem> + ))} + </DataList> + ) +} UnitListComponent.propTypes = { unitType: PropTypes.string.isRequired, diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListContainer.js new file mode 100644 index 00000000..2d994f97 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListContainer.js @@ -0,0 +1,56 @@ +/* + * 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 from 'react' +import { useDispatch, useSelector } from 'react-redux' +import UnitListComponent from './UnitListComponent' +import { deleteUnit } from '../../../../../redux/actions/topology/machine' + +const unitMapping = { + cpu: 'cpus', + gpu: 'gpus', + memory: 'memories', + storage: 'storages', +} + +const UnitListContainer = ({ unitType, ...props }) => { + const dispatch = useDispatch() + const units = useSelector((state) => { + const machine = + state.objects.machine[ + state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].machines[ + state.interactionLevel.position - 1 + ] + ] + return machine[unitMapping[unitType]].map((id) => state.objects[unitType][id]) + }) + const onDelete = (unit, unitType) => dispatch(deleteUnit(unitType, unit._id)) + + return <UnitListComponent {...props} units={units} unitType={unitType} onDelete={onDelete} /> +} + +UnitListContainer.propTypes = { + unitType: PropTypes.string.isRequired, +} + +export default UnitListContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js index ebb5f479..723ed2e2 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js @@ -1,85 +1,30 @@ import React, { useState } from 'react' -import { Nav, NavItem, NavLink, Row, TabContent, TabPane } from 'reactstrap' -import UnitAddContainer from '../../../../../containers/app/sidebars/topology/machine/UnitAddContainer' -import UnitListContainer from '../../../../../containers/app/sidebars/topology/machine/UnitListContainer' +import { Tab, Tabs, TabTitleText } from '@patternfly/react-core' +import UnitAddContainer from './UnitAddContainer' +import UnitListContainer from './UnitListContainer' const UnitTabsComponent = () => { const [activeTab, setActiveTab] = useState('cpu-units') - const toggle = (tab) => { - if (activeTab !== tab) setActiveTab(tab) - } return ( - <div className="mt-2"> - <Nav tabs> - <NavItem> - <NavLink - className={activeTab === 'cpu-units' ? 'active' : ''} - onClick={() => { - toggle('cpu-units') - }} - > - CPU - </NavLink> - </NavItem> - <NavItem> - <NavLink - className={activeTab === 'gpu-units' ? 'active' : ''} - onClick={() => { - toggle('gpu-units') - }} - > - GPU - </NavLink> - </NavItem> - <NavItem> - <NavLink - className={activeTab === 'memory-units' ? 'active' : ''} - onClick={() => { - toggle('memory-units') - }} - > - Memory - </NavLink> - </NavItem> - <NavItem> - <NavLink - className={activeTab === 'storage-units' ? 'active' : ''} - onClick={() => { - toggle('storage-units') - }} - > - Stor. - </NavLink> - </NavItem> - </Nav> - <TabContent activeTab={activeTab}> - <TabPane tabId="cpu-units"> - <div className="py-2"> - <UnitAddContainer unitType="cpu" /> - <UnitListContainer unitType="cpu" /> - </div> - </TabPane> - <TabPane tabId="gpu-units"> - <div className="py-2"> - <UnitAddContainer unitType="gpu" /> - <UnitListContainer unitType="gpu" /> - </div> - </TabPane> - <TabPane tabId="memory-units"> - <div className="py-2"> - <UnitAddContainer unitType="memory" /> - <UnitListContainer unitType="memory" /> - </div> - </TabPane> - <TabPane tabId="storage-units"> - <div className="py-2"> - <UnitAddContainer unitType="storage" /> - <UnitListContainer unitType="storage" /> - </div> - </TabPane> - </TabContent> - </div> + <Tabs activeKey={activeTab} onSelect={(_, tab) => setActiveTab(tab)}> + <Tab eventKey="cpu-units" title={<TabTitleText>CPU</TabTitleText>}> + <UnitAddContainer unitType="cpu" /> + <UnitListContainer unitType="cpu" /> + </Tab> + <Tab eventKey="gpu-units" title={<TabTitleText>GPU</TabTitleText>}> + <UnitAddContainer unitType="gpu" /> + <UnitListContainer unitType="gpu" /> + </Tab> + <Tab eventKey="memory-units" title={<TabTitleText>Memory</TabTitleText>}> + <UnitAddContainer unitType="memory" /> + <UnitListContainer unitType="memory" /> + </Tab> + <Tab eventKey="storage-units" title={<TabTitleText>Storage</TabTitleText>}> + <UnitAddContainer unitType="storage" /> + <UnitListContainer unitType="storage" /> + </Tab> + </Tabs> ) } diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js index a330c302..c8543134 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js @@ -1,12 +1,10 @@ import PropTypes from 'prop-types' import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faSave } from '@fortawesome/free-solid-svg-icons' -import { Button } from 'reactstrap' +import { SaveIcon } from '@patternfly/react-icons' +import { Button } from '@patternfly/react-core' const AddPrefabComponent = ({ onClick }) => ( - <Button color="primary" block onClick={onClick}> - <FontAwesomeIcon icon={faSave} className="mr-2" /> + <Button variant="primary" icon={<SaveIcon />} isBlock onClick={onClick} className="pf-u-mb-sm"> Save this rack to a prefab </Button> ) diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabContainer.js new file mode 100644 index 00000000..d3d9aaf5 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabContainer.js @@ -0,0 +1,33 @@ +/* + * 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 { useDispatch } from 'react-redux' +import { addPrefab } from '../../../../../redux/actions/prefabs' +import AddPrefabComponent from './AddPrefabComponent' + +const AddPrefabContainer = (props) => { + const dispatch = useDispatch() + return <AddPrefabComponent {...props} onClick={() => dispatch(addPrefab('name'))} /> +} + +export default AddPrefabContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js deleted file mode 100644 index e0eb5979..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js +++ /dev/null @@ -1,18 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faAngleLeft } from '@fortawesome/free-solid-svg-icons' -import { Button } from 'reactstrap' - -const BackToRoomComponent = ({ onClick }) => ( - <Button color="secondary" block className="mb-2" onClick={onClick}> - <FontAwesomeIcon icon={faAngleLeft} className="mr-2" /> - Back to room - </Button> -) - -BackToRoomComponent.propTypes = { - onClick: PropTypes.func, -} - -export default BackToRoomComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackContainer.js new file mode 100644 index 00000000..47959f03 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackContainer.js @@ -0,0 +1,54 @@ +/* + * 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, { useState } from 'react' +import { useDispatch } from 'react-redux' +import ConfirmationModal from '../../../../../components/modals/ConfirmationModal' +import { deleteRack } from '../../../../../redux/actions/topology/rack' +import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon' +import { Button } from '@patternfly/react-core' + +const DeleteRackContainer = () => { + const dispatch = useDispatch() + const [isVisible, setVisible] = useState(false) + const callback = (isConfirmed) => { + if (isConfirmed) { + dispatch(deleteRack()) + } + setVisible(false) + } + return ( + <> + <Button variant="danger" icon={<TrashIcon />} isBlock onClick={() => setVisible(true)}> + Delete this rack + </Button> + <ConfirmationModal + title="Delete this rack" + message="Are you sure you want to delete this rack?" + isOpen={isVisible} + callback={callback} + /> + </> + ) +} + +export default DeleteRackContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js deleted file mode 100644 index 63b319e0..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js +++ /dev/null @@ -1,24 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPlus } from '@fortawesome/free-solid-svg-icons' -import { ListGroupItem, Badge, Button } from 'reactstrap' - -const EmptySlotComponent = ({ position, onAdd }) => ( - <ListGroupItem className="d-flex justify-content-between align-items-center"> - <Badge color="info" className="mr-1"> - {position} - </Badge> - <Button color="primary" outline onClick={onAdd}> - <FontAwesomeIcon icon={faPlus} className="mr-2" /> - Add machine - </Button> - </ListGroupItem> -) - -EmptySlotComponent.propTypes = { - position: PropTypes.number, - onAdd: PropTypes.func, -} - -export default EmptySlotComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js index b71918da..1617b3bf 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js @@ -2,18 +2,16 @@ import PropTypes from 'prop-types' import React from 'react' import Image from 'next/image' import { Machine } from '../../../../../shapes' -import { Badge, ListGroupItem } from 'reactstrap' +import { Flex, Label } from '@patternfly/react-core' const UnitIcon = ({ id, type }) => ( - <div className="ml-1"> - <Image - src={'/img/topology/' + id + '-icon.png'} - alt={'Machine contains ' + type + ' units'} - layout="intrinsic" - height={35} - width={35} - /> - </div> + <Image + src={'/img/topology/' + id + '-icon.png'} + alt={'Machine contains ' + type + ' units'} + layout="intrinsic" + height={24} + width={24} + /> ) UnitIcon.propTypes = { @@ -21,34 +19,27 @@ UnitIcon.propTypes = { type: PropTypes.string, } -const MachineComponent = ({ position, machine, onClick }) => { +const MachineComponent = ({ machine, onClick }) => { const hasNoUnits = machine.cpus.length + machine.gpus.length + machine.memories.length + machine.storages.length === 0 return ( - <ListGroupItem - action - className="d-flex justify-content-between align-items-center" - onClick={onClick} - style={{ backgroundColor: 'white' }} - > - <Badge color="info" className="mr-1"> - {position} - </Badge> - <div className="d-inline-flex"> - {machine.cpus.length > 0 ? <UnitIcon id="cpu" type="CPU" /> : undefined} - {machine.gpus.length > 0 ? <UnitIcon id="gpu" type="GPU" /> : undefined} - {machine.memories.length > 0 ? <UnitIcon id="memory" type="memory" /> : undefined} - {machine.storages.length > 0 ? <UnitIcon id="storage" type="storage" /> : undefined} - {hasNoUnits ? <Badge color="warning">Machine with no units</Badge> : undefined} - </div> - </ListGroupItem> + <Flex onClick={() => onClick()}> + {machine.cpus.length > 0 ? <UnitIcon id="cpu" type="CPU" /> : undefined} + {machine.gpus.length > 0 ? <UnitIcon id="gpu" type="GPU" /> : undefined} + {machine.memories.length > 0 ? <UnitIcon id="memory" type="memory" /> : undefined} + {machine.storages.length > 0 ? <UnitIcon id="storage" type="storage" /> : undefined} + {hasNoUnits ? ( + <Label variant="outline" color="orange"> + Machine with no units + </Label> + ) : undefined} + </Flex> ) } MachineComponent.propTypes = { - machine: Machine, - position: PropTypes.number, + machine: Machine.isRequired, onClick: PropTypes.func, } diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js index e024a417..27834cf4 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js @@ -1,21 +1,66 @@ import PropTypes from 'prop-types' import React from 'react' -import { machineList } from './MachineListComponent.module.scss' import MachineComponent from './MachineComponent' import { Machine } from '../../../../../shapes' -import EmptySlotComponent from './EmptySlotComponent' +import { + Badge, + Button, + DataList, + DataListAction, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, +} from '@patternfly/react-core' +import { AngleRightIcon, PlusIcon } from '@patternfly/react-icons' -const MachineListComponent = ({ machines = [], onSelect, onAdd }) => { +function MachineListComponent({ machines = [], onSelect, onAdd }) { return ( - <ul className={`list-group ${machineList}`}> - {machines.map((machine, index) => { - if (machine === null) { - return <EmptySlotComponent key={index} onAdd={() => onAdd(index + 1)} /> - } else { - return <MachineComponent key={index} onClick={() => onSelect(index + 1)} machine={machine} /> - } - })} - </ul> + <DataList aria-label="Rack Units"> + {machines.map((machine, index) => + machine ? ( + <DataListItem key={index} onClick={() => onSelect(index + 1)}> + <DataListItemRow> + <DataListItemCells + dataListCells={[ + <DataListCell isIcon key="icon"> + <Badge isRead>{machines.length - index}U</Badge> + </DataListCell>, + <DataListCell key="primary content"> + <MachineComponent onClick={() => onSelect(index + 1)} machine={machine} /> + </DataListCell>, + ]} + /> + <DataListAction id="goto" aria-label="Goto Machine" aria-labelledby="goto"> + <Button isSmall variant="plain" className="pf-u-p-0"> + <AngleRightIcon /> + </Button> + </DataListAction> + </DataListItemRow> + </DataListItem> + ) : ( + <DataListItem key={index}> + <DataListItemRow> + <DataListItemCells + dataListCells={[ + <DataListCell isIcon key="icon"> + <Badge isRead>{machines.length - index}U</Badge> + </DataListCell>, + <DataListCell key="add" className="text-secondary"> + Empty Slot + </DataListCell>, + ]} + /> + <DataListAction id="add" aria-label="Add Machine" aria-labelledby="add"> + <Button isSmall variant="plain" className="pf-u-p-0" onClick={() => onAdd(index + 1)}> + <PlusIcon /> + </Button> + </DataListAction> + </DataListItemRow> + </DataListItem> + ) + )} + </DataList> ) } diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.module.scss b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.module.scss deleted file mode 100644 index f075aac9..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.machineList li { - min-height: 64px; -} diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListContainer.js new file mode 100644 index 00000000..54e6db0a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListContainer.js @@ -0,0 +1,56 @@ +/* + * 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, { useMemo } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import MachineListComponent from './MachineListComponent' +import { goFromRackToMachine } from '../../../../../redux/actions/interaction-level' +import { addMachine } from '../../../../../redux/actions/topology/rack' + +function MachineListContainer({ tileId, ...props }) { + const rack = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack]) + const machines = useSelector((state) => rack.machines.map((id) => state.objects.machine[id])) + const machinesNull = useMemo(() => { + const res = Array(rack.capacity).fill(null) + for (const machine of machines) { + res[machine.position - 1] = machine + } + return res + }, [rack, machines]) + const dispatch = useDispatch() + + return ( + <MachineListComponent + {...props} + machines={machinesNull} + onAdd={(index) => dispatch(addMachine(index))} + onSelect={(index) => dispatch(goFromRackToMachine(index))} + /> + ) +} + +MachineListContainer.propTypes = { + tileId: PropTypes.string.isRequired, +} + +export default MachineListContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameContainer.js new file mode 100644 index 00000000..11529b55 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameContainer.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import NameComponent from '../NameComponent' +import { editRackName } from '../../../../../redux/actions/topology/rack' + +const RackNameContainer = ({ tileId }) => { + const rackName = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack].name) + const dispatch = useDispatch() + const callback = (name) => { + if (name) { + dispatch(editRackName(name)) + } + } + return <NameComponent name={rackName} onEdit={callback} /> +} + +RackNameContainer.propTypes = { + tileId: PropTypes.string.isRequired, +} + +export default RackNameContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js index 74313bf7..dd5117f7 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js @@ -1,25 +1,58 @@ +import PropTypes from 'prop-types' import React from 'react' -import BackToRoomContainer from '../../../../../containers/app/sidebars/topology/rack/BackToRoomContainer' -import DeleteRackContainer from '../../../../../containers/app/sidebars/topology/rack/DeleteRackContainer' -import MachineListContainer from '../../../../../containers/app/sidebars/topology/rack/MachineListContainer' -import RackNameContainer from '../../../../../containers/app/sidebars/topology/rack/RackNameContainer' -import { sidebarContainer, sidebarHeaderContainer, machineListContainer } from './RackSidebarComponent.module.scss' -import AddPrefabContainer from '../../../../../containers/app/sidebars/topology/rack/AddPrefabContainer' +import { machineListContainer, sidebarContainer } from './RackSidebarComponent.module.scss' +import RackNameContainer from './RackNameContainer' +import AddPrefabContainer from './AddPrefabContainer' +import DeleteRackContainer from './DeleteRackContainer' +import MachineListContainer from './MachineListContainer' +import { + Skeleton, + TextContent, + TextList, + TextListItem, + TextListItemVariants, + TextListVariants, + Title, +} from '@patternfly/react-core' +import { useSelector } from 'react-redux' + +function RackSidebarComponent({ tileId }) { + const rack = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack]) -const RackSidebarComponent = () => { return ( - <div className={`${sidebarContainer} flex-column`}> - <div className={sidebarHeaderContainer}> - <RackNameContainer /> - <BackToRoomContainer /> + <div className={sidebarContainer}> + <TextContent> + <Title headingLevel="h2">Details</Title> + <TextList component={TextListVariants.dl}> + <TextListItem + component={TextListItemVariants.dt} + className="pf-u-display-inline-flex pf-u-align-items-center" + > + Name + </TextListItem> + <TextListItem component={TextListItemVariants.dd}> + <RackNameContainer tileId={tileId} /> + </TextListItem> + <TextListItem component={TextListItemVariants.dt}>Capacity</TextListItem> + <TextListItem component={TextListItemVariants.dd}> + {rack?.capacity ?? <Skeleton screenreaderText="Loading rack" />} + </TextListItem> + </TextList> + <Title headingLevel="h2">Actions</Title> <AddPrefabContainer /> <DeleteRackContainer /> - </div> - <div className={`${machineListContainer} mt-2`}> - <MachineListContainer /> + + <Title headingLevel="h2">Slots</Title> + </TextContent> + <div className={machineListContainer}> + <MachineListContainer tileId={tileId} /> </div> </div> ) } +RackSidebarComponent.propTypes = { + tileId: PropTypes.string.isRequired, +} + export default RackSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.module.scss b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.module.scss index 8ce3836a..6f258aec 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.module.scss +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.module.scss @@ -2,13 +2,11 @@ display: flex; height: 100%; max-height: 100%; -} - -.sidebarHeaderContainer { - flex: 0; + flex-direction: column; } .machineListContainer { flex: 1; overflow-y: scroll; + margin-top: 10px; } diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarContainer.js new file mode 100644 index 00000000..2b31413d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarContainer.js @@ -0,0 +1,32 @@ +/* + * 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 { useSelector } from 'react-redux' +import RackSidebarComponent from './RackSidebarComponent' + +const RackSidebarContainer = (props) => { + const tileId = useSelector((state) => state.interactionLevel.tileId) + return <RackSidebarComponent {...props} tileId={tileId} /> +} + +export default RackSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js deleted file mode 100644 index 043cc713..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js +++ /dev/null @@ -1,17 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faAngleLeft } from '@fortawesome/free-solid-svg-icons' - -const BackToBuildingComponent = ({ onClick }) => ( - <div className="btn btn-secondary btn-block mb-2" onClick={onClick}> - <FontAwesomeIcon icon={faAngleLeft} className="mr-2" /> - Back to building - </div> -) - -BackToBuildingComponent.propTypes = { - onClick: PropTypes.func, -} - -export default BackToBuildingComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js deleted file mode 100644 index d81bad0f..00000000 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js +++ /dev/null @@ -1,18 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faTrash } from '@fortawesome/free-solid-svg-icons' -import { Button } from 'reactstrap' - -const DeleteRoomComponent = ({ onClick }) => ( - <Button color="danger" outline block onClick={onClick}> - <FontAwesomeIcon icon={faTrash} className="mr-2" /> - Delete this room - </Button> -) - -DeleteRoomComponent.propTypes = { - onClick: PropTypes.func, -} - -export default DeleteRoomComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomContainer.js new file mode 100644 index 00000000..284c4d53 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomContainer.js @@ -0,0 +1,54 @@ +/* + * 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, { useState } from 'react' +import { useDispatch } from 'react-redux' +import ConfirmationModal from '../../../../../components/modals/ConfirmationModal' +import { deleteRoom } from '../../../../../redux/actions/topology/room' +import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon' +import { Button } from '@patternfly/react-core' + +const DeleteRoomContainer = () => { + const dispatch = useDispatch() + const [isVisible, setVisible] = useState(false) + const callback = (isConfirmed) => { + if (isConfirmed) { + dispatch(deleteRoom()) + } + setVisible(false) + } + return ( + <> + <Button variant="danger" icon={<TrashIcon />} isBlock onClick={() => setVisible(true)}> + Delete this room + </Button> + <ConfirmationModal + title="Delete this room" + message="Are you sure you want to delete this room?" + isOpen={isVisible} + callback={callback} + /> + </> + ) +} + +export default DeleteRoomContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomContainer.js new file mode 100644 index 00000000..6db2bfb6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomContainer.js @@ -0,0 +1,56 @@ +/* + * 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 { useDispatch, useSelector } from 'react-redux' +import { finishRoomEdit, startRoomEdit } from '../../../../../redux/actions/topology/building' +import CheckIcon from '@patternfly/react-icons/dist/js/icons/check-icon' +import PencilAltIcon from '@patternfly/react-icons/dist/js/icons/pencil-alt-icon' +import { Button } from '@patternfly/react-core' + +const EditRoomContainer = () => { + const isEditing = useSelector((state) => state.construction.currentRoomInConstruction !== '-1') + const isInRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode) + + const dispatch = useDispatch() + const onEdit = () => dispatch(startRoomEdit()) + const onFinish = () => dispatch(finishRoomEdit()) + + return isEditing ? ( + <Button variant="tertiary" icon={<CheckIcon />} isBlock onClick={onFinish} className="pf-u-mb-sm"> + Finish editing room + </Button> + ) : ( + <Button + variant="tertiary" + icon={<PencilAltIcon />} + isBlock + disabled={isInRackConstructionMode} + onClick={() => (isInRackConstructionMode ? undefined : onEdit())} + className="pf-u-mb-sm" + > + Edit the tiles of this room + </Button> + ) +} + +export default EditRoomContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js index 0a27910c..8aebe969 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js @@ -1,14 +1,13 @@ import PropTypes from 'prop-types' import React from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faTimes, faPlus } from '@fortawesome/free-solid-svg-icons' -import { Button } from 'reactstrap' +import { Button } from '@patternfly/react-core' +import PlusIcon from '@patternfly/react-icons/dist/js/icons/plus-icon' +import TimesIcon from '@patternfly/react-icons/dist/js/icons/times-icon' const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, isEditingRoom }) => { if (inRackConstructionMode) { return ( - <Button color="primary" block onClick={onStop}> - <FontAwesomeIcon icon={faTimes} className="mr-2" /> + <Button isBlock={true} icon={<TimesIcon />} onClick={onStop}> Stop rack construction </Button> ) @@ -16,13 +15,12 @@ const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, is return ( <Button - color="primary" - outline - block - disabled={isEditingRoom} + icon={<PlusIcon />} + isBlock + isDisabled={isEditingRoom} onClick={() => (isEditingRoom ? undefined : onStart())} + className="pf-u-mb-sm" > - <FontAwesomeIcon icon={faPlus} className="mr-2" /> Start rack construction </Button> ) diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionContainer.js new file mode 100644 index 00000000..38af447a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionContainer.js @@ -0,0 +1,46 @@ +/* + * 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 { useDispatch, useSelector } from 'react-redux' +import { startRackConstruction, stopRackConstruction } from '../../../../../redux/actions/topology/room' +import RackConstructionComponent from './RackConstructionComponent' + +const RackConstructionContainer = (props) => { + const isRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode) + const isEditingRoom = useSelector((state) => state.construction.currentRoomInConstruction !== '-1') + + const dispatch = useDispatch() + const onStart = () => dispatch(startRackConstruction()) + const onStop = () => dispatch(stopRackConstruction()) + return ( + <RackConstructionComponent + {...props} + inRackConstructionMode={isRackConstructionMode} + isEditingRoom={isEditingRoom} + onStart={onStart} + onStop={onStop} + /> + ) +} + +export default RackConstructionContainer diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomName.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomName.js new file mode 100644 index 00000000..d7b006a6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomName.js @@ -0,0 +1,44 @@ +/* + * 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 from 'react' +import { useDispatch, useSelector } from 'react-redux' +import NameComponent from '../../../../../components/app/sidebars/topology/NameComponent' +import { editRoomName } from '../../../../../redux/actions/topology/room' + +function RoomName({ roomId }) { + const roomName = useSelector((state) => state.objects.room[roomId].name) + const dispatch = useDispatch() + const callback = (name) => { + if (name) { + dispatch(editRoomName(name)) + } + } + return <NameComponent name={roomName} onEdit={callback} /> +} + +RoomName.propTypes = { + roomId: PropTypes.string.isRequired, +} + +export default RoomName diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js index 1bc6533e..fac58c51 100644 --- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js @@ -1,20 +1,43 @@ +import PropTypes from 'prop-types' import React from 'react' -import BackToBuildingContainer from '../../../../../containers/app/sidebars/topology/room/BackToBuildingContainer' -import DeleteRoomContainer from '../../../../../containers/app/sidebars/topology/room/DeleteRoomContainer' -import EditRoomContainer from '../../../../../containers/app/sidebars/topology/room/EditRoomContainer' -import RackConstructionContainer from '../../../../../containers/app/sidebars/topology/room/RackConstructionContainer' -import RoomNameContainer from '../../../../../containers/app/sidebars/topology/room/RoomNameContainer' +import RoomName from './RoomName' +import RackConstructionContainer from './RackConstructionContainer' +import EditRoomContainer from './EditRoomContainer' +import DeleteRoomContainer from './DeleteRoomContainer' +import { + TextContent, + TextList, + TextListItem, + TextListItemVariants, + TextListVariants, + Title, +} from '@patternfly/react-core' -const RoomSidebarComponent = () => { +const RoomSidebarComponent = ({ roomId }) => { return ( - <div> - <RoomNameContainer /> - <BackToBuildingContainer /> + <TextContent> + <Title headingLevel="h2">Details</Title> + <TextList component={TextListVariants.dl}> + <TextListItem + component={TextListItemVariants.dt} + className="pf-u-display-inline-flex pf-u-align-items-center" + > + Name + </TextListItem> + <TextListItem component={TextListItemVariants.dd}> + <RoomName roomId={roomId} /> + </TextListItem> + </TextList> + <Title headingLevel="h2">Construction</Title> <RackConstructionContainer /> <EditRoomContainer /> <DeleteRoomContainer /> - </div> + </TextContent> ) } +RoomSidebarComponent.propTypes = { + roomId: PropTypes.string.isRequired, +} + export default RoomSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarContainer.js new file mode 100644 index 00000000..2076b00e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarContainer.js @@ -0,0 +1,32 @@ +/* + * 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 { useSelector } from 'react-redux' +import RoomSidebarComponent from './RoomSidebarComponent' + +const RoomSidebarContainer = (props) => { + const roomId = useSelector((state) => state.interactionLevel.roomId) + return <RoomSidebarComponent {...props} roomId={roomId} /> +} + +export default RoomSidebarContainer 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 5a95810a..f6e1c98b 100644 --- a/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js +++ b/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js @@ -2,11 +2,11 @@ import PropTypes from 'prop-types' import React from 'react' import Modal from './Modal' -function ConfirmationModal({ title, message, show, callback }) { +function ConfirmationModal({ title, message, isOpen, callback }) { return ( <Modal title={title} - show={show} + isOpen={isOpen} onSubmit={() => callback(true)} onCancel={() => callback(false)} submitButtonType="danger" @@ -20,7 +20,7 @@ function ConfirmationModal({ title, message, show, callback }) { ConfirmationModal.propTypes = { title: PropTypes.string.isRequired, message: PropTypes.string.isRequired, - show: PropTypes.bool.isRequired, + isOpen: PropTypes.bool.isRequired, callback: PropTypes.func.isRequired, } diff --git a/opendc-web/opendc-web-ui/src/components/modals/Modal.js b/opendc-web/opendc-web-ui/src/components/modals/Modal.js index 8ab3924c..d4577062 100644 --- a/opendc-web/opendc-web-ui/src/components/modals/Modal.js +++ b/opendc-web/opendc-web-ui/src/components/modals/Modal.js @@ -1,43 +1,27 @@ -import React, { useState, useEffect } from 'react' +import React from 'react' import PropTypes from 'prop-types' -import { Modal as RModal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap' +import { Button, Modal as PModal, ModalVariant } from '@patternfly/react-core' -function Modal({ children, title, show, onSubmit, onCancel, submitButtonType, submitButtonText }) { - const [modal, setModal] = useState(show) - - useEffect(() => setModal(show), [show]) - - const toggle = () => setModal(!modal) - const cancel = () => { - if (onCancel() !== false) { - toggle() - } - } - const submit = () => { - if (onSubmit() !== false) { - toggle() - } - } +function Modal({ children, title, isOpen, onSubmit, onCancel, submitButtonType, submitButtonText }) { + const actions = [ + <Button variant={submitButtonType} onClick={onSubmit} key="confirm"> + {submitButtonText} + </Button>, + <Button variant="link" onClick={onCancel} key="cancel"> + Cancel + </Button>, + ] return ( - <RModal isOpen={modal} toggle={cancel}> - <ModalHeader toggle={cancel}>{title}</ModalHeader> - <ModalBody>{children}</ModalBody> - <ModalFooter> - <Button color="secondary" onClick={cancel}> - Close - </Button> - <Button color={submitButtonType} onClick={submit}> - {submitButtonText} - </Button> - </ModalFooter> - </RModal> + <PModal variant={ModalVariant.small} isOpen={isOpen} onClose={onCancel} title={title} actions={actions}> + {children} + </PModal> ) } Modal.propTypes = { title: PropTypes.string.isRequired, - show: PropTypes.bool.isRequired, + isOpen: PropTypes.bool, onSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, submitButtonType: PropTypes.string, @@ -48,7 +32,7 @@ Modal.propTypes = { Modal.defaultProps = { submitButtonType: 'primary', submitButtonText: 'Save', - show: false, + isOpen: false, } export default Modal diff --git a/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js b/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js index 6758fdc0..392a729e 100644 --- a/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js +++ b/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js @@ -1,31 +1,60 @@ import PropTypes from 'prop-types' -import React, { useRef } from 'react' +import React, { useRef, useState } from 'react' import Modal from './Modal' +import { Form, FormGroup, TextInput } from '@patternfly/react-core' -function TextInputModal({ title, label, show, callback, initialValue }) { +function TextInputModal({ title, label, isOpen, callback, initialValue }) { const textInput = useRef(null) - const onSubmit = () => { - callback(textInput.current.value) + const [isSubmitted, setSubmitted] = useState(false) + const [isValid, setValid] = useState(true) + + const resetState = () => { textInput.current.value = '' + setSubmitted(false) + setValid(false) + } + const onSubmit = (event) => { + const value = textInput.current.value + setSubmitted(true) + + if (event) { + event.preventDefault() + } + + if (!value) { + setValid(false) + return false + } + + callback(value) + resetState() + return true } const onCancel = () => { callback(undefined) - textInput.current.value = '' + resetState() } return ( - <Modal title={title} show={show} onSubmit={onSubmit} onCancel={onCancel}> - <form - onSubmit={(e) => { - e.preventDefault() - onSubmit() - }} - > - <div className="form-group"> - <label className="form-control-label">{label}</label> - <input type="text" className="form-control" ref={textInput} defaultValue={initialValue} /> - </div> - </form> + <Modal title={title} isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}> + <Form onSubmit={onSubmit}> + <FormGroup + label={label} + fieldId="text-input" + isRequired + validated={isSubmitted && !isValid ? 'error' : 'default'} + helperTextInvalid="This field cannot be empty" + > + <TextInput + id="text-input" + name="text-input" + isRequired + type="text" + ref={textInput} + defaultValue={initialValue} + /> + </FormGroup> + </Form> </Modal> ) } @@ -33,7 +62,7 @@ function TextInputModal({ title, label, show, callback, initialValue }) { TextInputModal.propTypes = { title: PropTypes.string.isRequired, label: PropTypes.string.isRequired, - show: PropTypes.bool.isRequired, + isOpen: PropTypes.bool.isRequired, callback: PropTypes.func.isRequired, initialValue: PropTypes.string, } diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModal.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModal.js new file mode 100644 index 00000000..afe07597 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModal.js @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types' +import React, { useRef, useState } from 'react' +import Modal from '../Modal' +import { + Form, + FormGroup, + FormSection, + NumberInput, + Select, + SelectGroup, + SelectOption, + SelectVariant, + TextInput, +} from '@patternfly/react-core' +import { METRIC_GROUPS, METRIC_NAMES } from '../../../util/available-metrics' + +const NewPortfolioModal = ({ isOpen, onSubmit: onSubmitUpstream, onCancel: onUpstreamCancel }) => { + const nameInput = useRef(null) + const [repeats, setRepeats] = useState(1) + const [isSelectOpen, setSelectOpen] = useState(false) + const [selectedMetrics, setSelectedMetrics] = useState([]) + + const [isSubmitted, setSubmitted] = useState(false) + const [errors, setErrors] = useState({}) + + const clearState = () => { + setSubmitted(false) + setErrors({}) + nameInput.current.value = '' + setRepeats(1) + setSelectOpen(false) + setSelectedMetrics([]) + } + + const onSubmit = (event) => { + setSubmitted(true) + + if (event) { + event.preventDefault() + } + + const name = nameInput.current.value + + if (!name) { + setErrors({ name: true }) + return false + } else { + onSubmitUpstream(name, { enabledMetrics: selectedMetrics, repeatsPerScenario: repeats }) + } + + clearState() + return false + } + const onCancel = () => { + onUpstreamCancel() + clearState() + } + + const onSelect = (event, selection) => { + if (selectedMetrics.includes(selection)) { + setSelectedMetrics((metrics) => metrics.filter((item) => item !== selection)) + } else { + setSelectedMetrics((metrics) => [...metrics, selection]) + } + } + + return ( + <Modal title="New Portfolio" isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}> + <Form onSubmit={onSubmit}> + <FormSection> + <FormGroup + label="Name" + fieldId="name" + isRequired + validated={isSubmitted && errors.name ? 'error' : 'default'} + helperTextInvalid="This field cannot be empty" + > + <TextInput + name="name" + id="name" + type="text" + isRequired + ref={nameInput} + placeholder="My Portfolio" + /> + </FormGroup> + </FormSection> + <FormSection title="Targets" titleElement="h4"> + <FormGroup label="Metrics" fieldId="metrics"> + <Select + variant={SelectVariant.typeaheadMulti} + typeAheadAriaLabel="Select a metric" + onToggle={() => setSelectOpen(!isSelectOpen)} + onSelect={onSelect} + onClear={() => setSelectedMetrics([])} + selections={selectedMetrics} + isOpen={isSelectOpen} + placeholderText="Select a metric" + menuAppendTo="parent" + maxHeight="300px" + chipGroupProps={{ numChips: 1 }} + isGrouped + > + {Object.entries(METRIC_GROUPS).map(([group, metrics]) => ( + <SelectGroup label={group} key={group}> + {metrics.map((metric) => ( + <SelectOption key={metric} value={metric}> + {METRIC_NAMES[metric]} + </SelectOption> + ))} + </SelectGroup> + ))} + </Select> + </FormGroup> + <FormGroup label="Repeats per Scenario" fieldId="repeats"> + <NumberInput + id="repeats" + inputName="repeats" + type="number" + value={repeats} + onChange={(e) => setRepeats(Number(e.target.value))} + onPlus={() => setRepeats((r) => r + 1)} + onMinus={() => setRepeats((r) => r - 1)} + min={1} + /> + </FormGroup> + </FormSection> + </Form> + </Modal> + ) +} + +NewPortfolioModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onSubmit: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +} + +export default NewPortfolioModal diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js deleted file mode 100644 index 3c6b8724..00000000 --- a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js +++ /dev/null @@ -1,78 +0,0 @@ -import PropTypes from 'prop-types' -import React, { useRef } from 'react' -import { Form, FormGroup, Input, Label } from 'reactstrap' -import Modal from '../Modal' -import { AVAILABLE_METRICS, METRIC_NAMES } from '../../../util/available-metrics' - -const NewPortfolioModalComponent = ({ show, callback }) => { - const form = useRef(null) - const textInput = useRef(null) - const repeatsInput = useRef(null) - const metricCheckboxes = useRef({}) - - const onSubmit = () => { - if (form.current.reportValidity()) { - callback(textInput.current.value, { - enabledMetrics: AVAILABLE_METRICS.filter((metric) => metricCheckboxes.current[metric].checked), - repeatsPerScenario: parseInt(repeatsInput.current.value), - }) - - return true - } else { - return false - } - } - const onCancel = () => callback(undefined) - - return ( - <Modal title="New Portfolio" show={show} onSubmit={onSubmit} onCancel={onCancel}> - <Form - onSubmit={(e) => { - e.preventDefault() - this.onSubmit() - }} - innerRef={form} - > - <FormGroup> - <Label for="name">Name</Label> - <Input name="name" type="text" required innerRef={textInput} placeholder="My Portfolio" /> - </FormGroup> - <h4>Targets</h4> - <h5>Metrics</h5> - <FormGroup> - {AVAILABLE_METRICS.map((metric) => ( - <FormGroup check key={metric}> - <Label for={metric} check> - <Input - name={metric} - type="checkbox" - innerRef={(ref) => (metricCheckboxes.current[metric] = ref)} - /> - {METRIC_NAMES[metric]} - </Label> - </FormGroup> - ))} - </FormGroup> - <FormGroup> - <Label for="repeats">Repeats per scenario</Label> - <Input - name="repeats" - type="number" - required - innerRef={repeatsInput} - defaultValue="1" - min="1" - step="1" - /> - </FormGroup> - </Form> - </Modal> - ) -} - -NewPortfolioModalComponent.propTypes = { - show: PropTypes.bool.isRequired, - callback: PropTypes.func.isRequired, -} - -export default NewPortfolioModalComponent diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModal.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModal.js new file mode 100644 index 00000000..94d0d424 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModal.js @@ -0,0 +1,159 @@ +import PropTypes from 'prop-types' +import React, { useRef, useState } from 'react' +import { Portfolio } from '../../../shapes' +import Modal from '../Modal' +import { + Checkbox, + Form, + FormGroup, + FormSection, + FormSelect, + FormSelectOption, + NumberInput, + TextInput, +} from '@patternfly/react-core' +import { useSchedulers, useTraces } from '../../../data/experiments' +import { useProjectTopologies } from '../../../data/topology' +import { usePortfolio } from '../../../data/project' + +const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) => { + const { data: portfolio } = usePortfolio(portfolioId) + const { data: topologies = [] } = useProjectTopologies(portfolio?.projectId) + const { data: traces = [] } = useTraces() + const { data: schedulers = [] } = useSchedulers() + + const [isSubmitted, setSubmitted] = useState(false) + const [traceLoad, setTraceLoad] = useState(100) + const [trace, setTrace] = useState(undefined) + const [topology, setTopology] = useState(undefined) + const [scheduler, setScheduler] = useState(undefined) + const [failuresEnabled, setFailuresEnabled] = useState(false) + const [opPhenEnabled, setOpPhenEnabled] = useState(false) + const nameInput = useRef(null) + + const resetState = () => { + setSubmitted(false) + setTraceLoad(100) + setTrace(undefined) + setTopology(undefined) + setScheduler(undefined) + setFailuresEnabled(false) + setOpPhenEnabled(false) + nameInput.current.value = '' + } + + const onSubmit = (event) => { + setSubmitted(true) + + if (event) { + event.preventDefault() + } + + const name = nameInput.current.value + + onSubmitUpstream( + name, + portfolio._id, + { + traceId: trace || traces[0]._id, + loadSamplingFraction: traceLoad / 100, + }, + { + topologyId: topology || topologies[0]._id, + }, + { + failuresEnabled, + performanceInterferenceEnabled: opPhenEnabled, + schedulerName: scheduler || schedulers[0].name, + } + ) + + resetState() + return true + } + const onCancel = () => { + onCancelUpstream() + resetState() + } + + return ( + <Modal title="New Scenario" isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}> + <Form onSubmit={onSubmit}> + <FormGroup label="Name" fieldId="name" isRequired> + <TextInput + id="name" + name="name" + type="text" + isDisabled={portfolio?.scenarioIds?.length === 0} + defaultValue={portfolio?.scenarioIds?.length === 0 ? 'Base scenario' : ''} + ref={nameInput} + /> + </FormGroup> + <FormSection title="Workload"> + <FormGroup label="Trace" fieldId="trace" isRequired> + <FormSelect id="trace" name="trace" value={trace} onChange={setTrace}> + {traces.map((trace) => ( + <FormSelectOption value={trace._id} key={trace._id} label={trace.name} /> + ))} + </FormSelect> + </FormGroup> + <FormGroup label="Load Sampling Fraction" fieldId="trace-load" isRequired> + <NumberInput + name="trace-load" + type="number" + min={0} + max={100} + value={traceLoad} + onMinus={() => setTraceLoad((load) => load - 1)} + onPlus={() => setTraceLoad((load) => load + 1)} + onChange={(e) => setTraceLoad(Number(e.target.value))} + unit="%" + /> + </FormGroup> + </FormSection> + <FormSection title="Topology"> + <FormGroup label="Topology" fieldId="topology" isRequired> + <FormSelect id="topology" name="topology" value={topology} onChange={setTopology}> + {topologies.map((topology) => ( + <FormSelectOption value={topology._id} key={topology._id} label={topology.name} /> + ))} + </FormSelect> + </FormGroup> + + <FormGroup label="Scheduler" fieldId="scheduler" isRequired> + <FormSelect id="scheduler" name="scheduler" value={scheduler} onChange={setScheduler}> + {schedulers.map((scheduler) => ( + <FormSelectOption value={scheduler.name} key={scheduler.name} label={scheduler.name} /> + ))} + </FormSelect> + </FormGroup> + </FormSection> + <FormSection title="Operational Phenomena"> + <Checkbox + label="Failures" + id="failures" + name="failures" + isChecked={failuresEnabled} + onChange={() => setFailuresEnabled((e) => !e)} + /> + <Checkbox + label="Performance Interference" + id="perf-interference" + name="perf-interference" + isChecked={opPhenEnabled} + onChange={() => setOpPhenEnabled((e) => !e)} + /> + </FormSection> + </Form> + </Modal> + ) +} + +NewScenarioModal.propTypes = { + portfolioId: PropTypes.string, + isOpen: PropTypes.bool.isRequired, + onSubmit: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +} + +export default NewScenarioModal diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js deleted file mode 100644 index 782812ac..00000000 --- a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js +++ /dev/null @@ -1,144 +0,0 @@ -import PropTypes from 'prop-types' -import React, { useRef } from 'react' -import { Form, FormGroup, Input, Label } from 'reactstrap' -import { Scheduler, Topology, Trace } from '../../../shapes' -import Modal from '../Modal' - -const NewScenarioModalComponent = ({ - show, - callback, - currentPortfolioId, - currentPortfolioScenarioIds, - traces, - topologies, - schedulers, -}) => { - const form = useRef(null) - const textInput = useRef(null) - const traceSelect = useRef(null) - const traceLoadInput = useRef(null) - const topologySelect = useRef(null) - const failuresCheckbox = useRef(null) - const performanceInterferenceCheckbox = useRef(null) - const schedulerSelect = useRef(null) - - const onSubmit = () => { - if (!form.current.reportValidity()) { - return false - } - callback( - textInput.current.value, - currentPortfolioId, - { - traceId: traceSelect.current.value, - loadSamplingFraction: parseFloat(traceLoadInput.current.value), - }, - { - topologyId: topologySelect.current.value, - }, - { - failuresEnabled: failuresCheckbox.current.checked, - performanceInterferenceEnabled: performanceInterferenceCheckbox.current.checked, - schedulerName: schedulerSelect.current.value, - } - ) - return true - } - const onCancel = () => { - callback(undefined) - } - - return ( - <Modal title="New Scenario" show={show} onSubmit={onSubmit} onCancel={onCancel}> - <Form - onSubmit={(e) => { - e.preventDefault() - onSubmit() - }} - innerRef={form} - > - <FormGroup> - <Label for="name">Name</Label> - <Input - name="name" - type="text" - required - disabled={currentPortfolioScenarioIds.length === 0} - defaultValue={currentPortfolioScenarioIds.length === 0 ? 'Base scenario' : ''} - innerRef={textInput} - /> - </FormGroup> - <h4>Trace</h4> - <FormGroup> - <Label for="trace">Trace</Label> - <Input name="trace" type="select" innerRef={traceSelect}> - {traces.map((trace) => ( - <option value={trace._id} key={trace._id}> - {trace.name} - </option> - ))} - </Input> - </FormGroup> - <FormGroup> - <Label for="trace-load">Load sampling fraction</Label> - <Input - name="trace-load" - type="number" - innerRef={traceLoadInput} - required - defaultValue="1" - min="0" - max="1" - step="0.1" - /> - </FormGroup> - <h4>Topology</h4> - <div className="form-group"> - <Label for="topology">Topology</Label> - <Input name="topology" type="select" innerRef={topologySelect}> - {topologies.map((topology) => ( - <option value={topology._id} key={topology._id}> - {topology.name} - </option> - ))} - </Input> - </div> - <h4>Operational Phenomena</h4> - <FormGroup check> - <Label check for="failures"> - <Input type="checkbox" name="failures" innerRef={failuresCheckbox} />{' '} - <span className="ml-2">Enable failures</span> - </Label> - </FormGroup> - <FormGroup check> - <Label check for="perf-interference"> - <Input type="checkbox" name="perf-interference" innerRef={performanceInterferenceCheckbox} />{' '} - <span className="ml-2">Enable performance interference</span> - </Label> - </FormGroup> - <FormGroup> - <Label for="scheduler">Scheduler</Label> - <Input name="scheduler" type="select" innerRef={schedulerSelect}> - {schedulers.map((scheduler) => ( - <option value={scheduler.name} key={scheduler.name}> - {scheduler.name} - </option> - ))} - </Input> - </FormGroup> - </Form> - </Modal> - ) -} - -NewScenarioModalComponent.propTypes = { - show: PropTypes.bool.isRequired, - currentPortfolioId: PropTypes.string.isRequired, - currentPortfolioScenarioIds: PropTypes.arrayOf(PropTypes.string), - traces: PropTypes.arrayOf(Trace), - topologies: PropTypes.arrayOf(Topology), - schedulers: PropTypes.arrayOf(Scheduler), - callback: PropTypes.func.isRequired, -} - -export default NewScenarioModalComponent diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModal.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModal.js new file mode 100644 index 00000000..49952aec --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModal.js @@ -0,0 +1,81 @@ +import PropTypes from 'prop-types' +import React, { useRef, useState } from 'react' +import Modal from '../Modal' +import { Form, FormGroup, FormSelect, FormSelectOption, TextInput } from '@patternfly/react-core' +import { useProjectTopologies } from '../../../data/topology' + +const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) => { + const nameInput = useRef(null) + const [isSubmitted, setSubmitted] = useState(false) + const [originTopology, setOriginTopology] = useState(-1) + const [errors, setErrors] = useState({}) + + const { data: topologies = [] } = useProjectTopologies(projectId) + + const clearState = () => { + nameInput.current.value = '' + setSubmitted(false) + setOriginTopology(-1) + setErrors({}) + } + + const onSubmit = (event) => { + setSubmitted(true) + + if (event) { + event.preventDefault() + } + + const name = nameInput.current.value + + if (!name) { + setErrors({ name: true }) + return false + } else if (originTopology === -1) { + onSubmitUpstream(name) + } else { + onSubmitUpstream(name, originTopology) + } + + clearState() + return true + } + + const onCancel = () => { + onCancelUpstream() + clearState() + } + + return ( + <Modal title="New Topology" isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}> + <Form onSubmit={onSubmit}> + <FormGroup + label="Name" + fieldId="name" + isRequired + validated={isSubmitted && errors.name ? 'error' : 'default'} + helperTextInvalid="This field cannot be empty" + > + <TextInput id="name" name="name" type="text" isRequired ref={nameInput} /> + </FormGroup> + <FormGroup label="Topology to duplicate" fieldId="origin" isRequired> + <FormSelect id="origin" name="origin" value={originTopology} onChange={setOriginTopology}> + <FormSelectOption value={-1} key={-1} label="None - start from scratch" /> + {topologies.map((topology) => ( + <FormSelectOption value={topology._id} key={topology._id} label={topology.name} /> + ))} + </FormSelect> + </FormGroup> + </Form> + </Modal> + ) +} + +NewTopologyModal.propTypes = { + projectId: PropTypes.string, + isOpen: PropTypes.bool.isRequired, + onSubmit: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +} + +export default NewTopologyModal diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js deleted file mode 100644 index f06fe797..00000000 --- a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js +++ /dev/null @@ -1,71 +0,0 @@ -import PropTypes from 'prop-types' -import { Form, FormGroup, Input, Label } from 'reactstrap' -import React, { useRef } from 'react' -import { Topology } from '../../../shapes' -import Modal from '../Modal' - -const NewTopologyModalComponent = ({ show, onCreateTopology, onDuplicateTopology, onCancel, topologies }) => { - const form = useRef(null) - const textInput = useRef(null) - const originTopology = useRef(null) - - const onCreate = () => { - onCreateTopology(textInput.current.value) - } - - const onDuplicate = () => { - onDuplicateTopology(textInput.current.value, originTopology.current.value) - } - - const onSubmit = () => { - if (!form.current.reportValidity()) { - return false - } else if (originTopology.current.selectedIndex === 0) { - onCreate() - } else { - onDuplicate() - } - - return true - } - - return ( - <Modal title="New Topology" show={show} onSubmit={onSubmit} onCancel={onCancel}> - <Form - onSubmit={(e) => { - e.preventDefault() - onSubmit() - }} - innerRef={form} - > - <FormGroup> - <Label for="name">Name</Label> - <Input name="name" type="text" required innerRef={textInput} /> - </FormGroup> - <FormGroup> - <Label for="origin">Topology to duplicate</Label> - <Input name="origin" type="select" innerRef={originTopology}> - <option value={-1} key={-1}> - None - start from scratch - </option> - {topologies.map((topology) => ( - <option value={topology._id} key={topology._id}> - {topology.name} - </option> - ))} - </Input> - </FormGroup> - </Form> - </Modal> - ) -} - -NewTopologyModalComponent.propTypes = { - show: PropTypes.bool.isRequired, - topologies: PropTypes.arrayOf(Topology), - onCreateTopology: PropTypes.func.isRequired, - onDuplicateTopology: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, -} - -export default NewTopologyModalComponent diff --git a/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js b/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js deleted file mode 100644 index 8aaaf847..00000000 --- a/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js +++ /dev/null @@ -1,37 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import Link from 'next/link' -import { NavLink, NavItem as RNavItem } from 'reactstrap' -import Navbar, { NavItem } from './Navbar' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faList } from '@fortawesome/free-solid-svg-icons' -import { Project } from '../../shapes' - -const AppNavbarComponent = ({ project, fullWidth }) => ( - <Navbar fullWidth={fullWidth}> - <NavItem route="/projects"> - <Link href="/projects" passHref> - <NavLink title="My Projects"> - <FontAwesomeIcon icon={faList} className="mr-2" /> - My Projects - </NavLink> - </Link> - </NavItem> - {project ? ( - <RNavItem> - <Link href={`/projects/${project._id}`} passHref> - <NavLink title="Current Project"> - <span>{project.name}</span> - </NavLink> - </Link> - </RNavItem> - ) : undefined} - </Navbar> -) - -AppNavbarComponent.propTypes = { - project: Project, - fullWidth: PropTypes.bool, -} - -export default AppNavbarComponent diff --git a/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js b/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js deleted file mode 100644 index 4ab577e0..00000000 --- a/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js +++ /dev/null @@ -1,17 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { NavLink } from 'reactstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons' - -const LogoutButton = ({ onLogout }) => ( - <NavLink className="logout" title="Sign out" onClick={onLogout}> - <FontAwesomeIcon icon={faSignOutAlt} size="lg" /> - </NavLink> -) - -LogoutButton.propTypes = { - onLogout: PropTypes.func.isRequired, -} - -export default LogoutButton diff --git a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js deleted file mode 100644 index dc74bb8f..00000000 --- a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js +++ /dev/null @@ -1,120 +0,0 @@ -import PropTypes from 'prop-types' -import React, { useState } from 'react' -import Link from 'next/link' -import { useRouter } from 'next/router' -import Image from 'next/image' -import { - Navbar as RNavbar, - NavItem as RNavItem, - NavLink, - NavbarBrand, - NavbarToggler, - Collapse, - Nav, - Container, -} from 'reactstrap' -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' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faGithub } from '@fortawesome/free-brands-svg-icons' - -export const NAVBAR_HEIGHT = 60 - -const GitHubLink = () => ( - <a - href="https://github.com/atlarge-research/opendc" - className="ml-2 mr-3 text-dark" - style={{ position: 'relative', top: 7 }} - > - <FontAwesomeIcon icon={faGithub} size="2x" /> - </a> -) - -export const NavItem = ({ route, children }) => { - const router = useRouter() - const handleClick = (e) => { - e.preventDefault() - router.push(route) - } - return ( - <RNavItem onClick={handleClick} active={router.asPath === route}> - {children} - </RNavItem> - ) -} - -NavItem.propTypes = { - route: PropTypes.string.isRequired, - children: PropTypes.node, -} - -export const LoggedInSection = () => { - const router = useRouter() - const { isAuthenticated } = useAuth() - return ( - <Nav navbar className="auth-links"> - {isAuthenticated ? ( - [ - router.asPath === '/' ? ( - <NavItem route="/projects" key="projects"> - <Link href="/projects" passHref> - <NavLink title="My Projects" to="/projects"> - My Projects - </NavLink> - </Link> - </NavItem> - ) : ( - <RNavItem key="profile"> - <NavLink title="My Profile"> - <ProfileName /> - </NavLink> - </RNavItem> - ), - <NavItem route="logout" key="logout"> - <Logout /> - </NavItem>, - ] - ) : ( - <RNavItem> - <GitHubLink /> - <Login visible={true} className={login} /> - </RNavItem> - )} - </Nav> - ) -} - -const Navbar = ({ fullWidth, children }) => { - const [isOpen, setIsOpen] = useState(false) - const toggle = () => setIsOpen(!isOpen) - - return ( - <RNavbar fixed="top" color="light" light expand="lg" id="navbar" className={navbar}> - <Container fluid={fullWidth}> - <NavbarToggler onClick={toggle} /> - <NavbarBrand href="/" title="OpenDC" className={opendcBrand}> - <div className="mb-n1"> - <Image src="/img/logo.png" layout="fixed" width={30} height={30} alt="OpenDC" /> - </div> - </NavbarBrand> - - <Collapse isOpen={isOpen} navbar> - <Nav className="mr-auto" navbar> - {children} - </Nav> - <LoggedInSection /> - </Collapse> - </Container> - </RNavbar> - ) -} - -Navbar.propTypes = { - fullWidth: PropTypes.bool, - children: PropTypes.node, -} - -export default Navbar diff --git a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.module.scss b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.module.scss deleted file mode 100644 index 8b9e4c97..00000000 --- a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.module.scss +++ /dev/null @@ -1,36 +0,0 @@ -@import 'src/style/_mixins.scss'; -@import 'src/style/_variables.scss'; - -.navbar { - border-top: $blue 3px solid; - border-bottom: $gray-semi-dark 1px solid; - color: $gray-very-dark; - background: #fafafb; -} - -.opendcBrand { - display: inline-block; - color: $gray-very-dark; - - transition: background $transition-length; - - img { - position: relative; - bottom: 3px; - display: inline-block; - width: 30px; - } -} - -.login { - height: 40px; - background: $blue; - border: none; - padding-top: 10px; - - @include clickable; - - &:hover { - background: $blue-dark; - } -} diff --git a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js b/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js deleted file mode 100644 index 03a4894b..00000000 --- a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import { blinkingCursor } from './BlinkingCursor.module.scss' - -const BlinkingCursor = () => <span className={blinkingCursor}>_</span> - -export default BlinkingCursor diff --git a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.module.scss b/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.module.scss deleted file mode 100644 index aba0c604..00000000 --- a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -.blinkingCursor { - animation: blink 1s step-end infinite; -} - -@keyframes blink { - from, - to { - color: #eeeeee; - } - 50% { - color: #333333; - } -} diff --git a/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js deleted file mode 100644 index 6ded4350..00000000 --- a/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react' -import { codeBlock } from './CodeBlock.module.scss' - -const CodeBlock = () => { - const textBlock = - ' oo oooo oo <br/>' + - ' oo oo oo oo <br/>' + - ' oo oo oo oo <br/>' + - ' oooooo oo oo oooooo <br/>' + - ' oo oo oo oo <br/>' + - ' oo oooo oo <br/>' - const charList = textBlock.split('') - - // Binary representation of the string "OpenDC!" ;) - const binaryString = '01001111011100000110010101101110010001000100001100100001' - - let binaryIndex = 0 - for (let i = 0; i < charList.length; i++) { - if (charList[i] === 'o') { - charList[i] = binaryString[binaryIndex] - binaryIndex++ - } - } - - return <div className={codeBlock} dangerouslySetInnerHTML={{ __html: textBlock }} /> -} - -export default CodeBlock diff --git a/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.module.scss b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.module.scss deleted file mode 100644 index 8af3ee6d..00000000 --- a/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.module.scss +++ /dev/null @@ -1,4 +0,0 @@ -.codeBlock { - white-space: pre-wrap; - margin-top: 60px; -} 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 deleted file mode 100644 index e6200b10..00000000 --- a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react' -import Link from 'next/link' -import BlinkingCursor from './BlinkingCursor' -import CodeBlock from './CodeBlock' -import { terminalWindow, terminalHeader, terminalBody, segfault, subTitle, homeBtn } from './TerminalWindow.module.scss' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faHome } from '@fortawesome/free-solid-svg-icons' - -const TerminalWindow = () => ( - <div className={terminalWindow}> - <div className={terminalHeader}>Terminal -- bash</div> - <div className={terminalBody}> - <div className={segfault}> - $ status - <br /> - opendc[4264]: segfault at 0000051497be459d1 err 12 in libopendc.9.0.4 - <br /> - opendc[4269]: segfault at 000004234855fc2db err 3 in libopendc.9.0.4 - <br /> - opendc[4270]: STDERR Page does not exist - <br /> - </div> - <CodeBlock /> - <div className={subTitle}> - Got lost? - <BlinkingCursor /> - </div> - <Link href="/"> - <a className={homeBtn}> - <FontAwesomeIcon icon={faHome} /> GET ME BACK TO OPENDC - </a> - </Link> - </div> - </div> -) - -export default TerminalWindow diff --git a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.module.scss b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.module.scss deleted file mode 100644 index 614852d3..00000000 --- a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.module.scss +++ /dev/null @@ -1,61 +0,0 @@ -.terminalWindow { - display: block; - align-self: center; - - margin: auto; - - user-select: none; - cursor: default; - - overflow: hidden; - - box-shadow: 5px 5px 20px #444444; -} - -.terminalHeader { - font-family: monospace; - background: #cccccc; - color: #444444; - height: 30px; - line-height: 30px; - padding-left: 10px; - - border-top-left-radius: 7px; - border-top-right-radius: 7px; -} - -.terminalBody { - font-family: monospace; - text-align: center; - background-color: #333333; - color: #eeeeee; - padding: 10px; - - height: 100%; -} - -.segfault { - text-align: left; -} - -.subTitle { - margin-top: 20px; -} - -.homeBtn { - margin-top: 10px; - padding: 5px; - display: inline-block; - border: 1px solid #eeeeee; - color: #eeeeee; - text-decoration: none; - cursor: pointer; - - transition: all 200ms; - - &:hover, - &:active { - background: #eeeeee; - color: #333333; - } -} diff --git a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js index 5129c013..285217e9 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js +++ b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js @@ -1,23 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' -import { Button, ButtonGroup } from 'reactstrap' +import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core' import { filterPanel } from './FilterPanel.module.scss' export const FILTERS = { SHOW_ALL: 'All Projects', SHOW_OWN: 'My Projects', SHOW_SHARED: 'Shared with me' } const FilterPanel = ({ onSelect, activeFilter = 'SHOW_ALL' }) => ( - <ButtonGroup className={`${filterPanel} mb-2`}> + <ToggleGroup className={`${filterPanel} mb-2`}> {Object.keys(FILTERS).map((filter) => ( - <Button - color="secondary" + <ToggleGroupItem key={filter} - onClick={() => activeFilter === filter || onSelect(filter)} - active={activeFilter === filter} - > - {FILTERS[filter]} - </Button> + onChange={() => activeFilter === filter || onSelect(filter)} + isSelected={activeFilter === filter} + text={FILTERS[filter]} + /> ))} - </ButtonGroup> + </ToggleGroup> ) FilterPanel.propTypes = { diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js new file mode 100644 index 00000000..ae4cb9cd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js @@ -0,0 +1,53 @@ +/* + * 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 { PlusIcon } from '@patternfly/react-icons' +import { Button } from '@patternfly/react-core' +import { useState } from 'react' +import NewPortfolioModal from '../modals/custom-components/NewPortfolioModal' +import { useMutation } from 'react-query' + +function NewPortfolio({ projectId }) { + const [isVisible, setVisible] = useState(false) + const { mutate: addPortfolio } = useMutation('addPortfolio') + + const onSubmit = (name, targets) => { + addPortfolio({ projectId, name, targets }) + setVisible(false) + } + + return ( + <> + <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}> + New Portfolio + </Button> + <NewPortfolioModal isOpen={isVisible} onSubmit={onSubmit} onCancel={() => setVisible(false)} /> + </> + ) +} + +NewPortfolio.propTypes = { + projectId: PropTypes.string, +} + +export default NewPortfolio diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewProject.js b/opendc-web/opendc-web-ui/src/components/projects/NewProject.js new file mode 100644 index 00000000..4f5d79cf --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/NewProject.js @@ -0,0 +1,39 @@ +import React, { useState } from 'react' +import TextInputModal from '../../components/modals/TextInputModal' +import { Button } from '@patternfly/react-core' +import { useMutation } from 'react-query' +import { PlusIcon } from '@patternfly/react-icons' +import { buttonContainer } from './NewProject.module.scss' + +/** + * A container for creating a new project. + */ +const NewProject = () => { + const [isVisible, setVisible] = useState(false) + const { mutate: addProject } = useMutation('addProject') + + const onSubmit = (name) => { + if (name) { + addProject({ name }) + } + setVisible(false) + } + + return ( + <> + <div className={buttonContainer}> + <Button + icon={<PlusIcon />} + color="primary" + className="pf-u-float-right" + onClick={() => setVisible(true)} + > + New Project + </Button> + </div> + <TextInputModal title="New Project" label="Project name" isOpen={isVisible} callback={onSubmit} /> + </> + ) +} + +export default NewProject diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss b/opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss new file mode 100644 index 00000000..5a0e74fc --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss @@ -0,0 +1,26 @@ +/*! + * 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. + */ + +.buttonContainer { + flex: 0 1 auto; + padding: 20px 0; +} diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewScenario.js b/opendc-web/opendc-web-ui/src/components/projects/NewScenario.js new file mode 100644 index 00000000..6d4f48c1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/NewScenario.js @@ -0,0 +1,64 @@ +/* + * 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 { PlusIcon } from '@patternfly/react-icons' +import { Button } from '@patternfly/react-core' +import { useState } from 'react' +import NewScenarioModal from '../modals/custom-components/NewScenarioModal' +import { useMutation } from 'react-query' + +function NewScenario({ portfolioId }) { + const [isVisible, setVisible] = useState(false) + const { mutate: addScenario } = useMutation('addScenario') + + const onSubmit = (name, portfolioId, trace, topology, operational) => { + addScenario({ + portfolioId, + name, + trace, + topology, + operational, + }) + setVisible(false) + } + + return ( + <> + <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}> + New Scenario + </Button> + <NewScenarioModal + portfolioId={portfolioId} + isOpen={isVisible} + onSubmit={onSubmit} + onCancel={() => setVisible(false)} + /> + </> + ) +} + +NewScenario.propTypes = { + portfolioId: PropTypes.string, +} + +export default NewScenario diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js b/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js new file mode 100644 index 00000000..c6c4171b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js @@ -0,0 +1,58 @@ +/* + * 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 { PlusIcon } from '@patternfly/react-icons' +import { Button } from '@patternfly/react-core' +import { useState } from 'react' +import NewTopologyModal from '../modals/custom-components/NewTopologyModal' +import { useDispatch } from 'react-redux' +import { addTopology } from '../../redux/actions/topologies' + +function NewTopology({ projectId }) { + const [isVisible, setVisible] = useState(false) + const dispatch = useDispatch() + + const onSubmit = (name, duplicateId) => { + dispatch(addTopology(projectId, name, duplicateId)) + setVisible(false) + } + return ( + <> + <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}> + New Topology + </Button> + <NewTopologyModal + projectId={projectId} + isOpen={isVisible} + onSubmit={onSubmit} + onCancel={() => setVisible(false)} + /> + </> + ) +} + +NewTopology.propTypes = { + projectId: PropTypes.string, +} + +export default NewTopology diff --git a/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js new file mode 100644 index 00000000..45e399ed --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js @@ -0,0 +1,97 @@ +/* + * 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 Link from 'next/link' +import { Table, TableBody, TableHeader } from '@patternfly/react-table' +import React from 'react' +import TableEmptyState from '../util/TableEmptyState' +import { useProjectPortfolios } from '../../data/project' +import { useMutation } from 'react-query' + +const PortfolioTable = ({ projectId }) => { + const { status, data: portfolios = [] } = useProjectPortfolios(projectId) + const { mutate: deletePortfolio } = useMutation('deletePortfolio') + + const columns = ['Name', 'Scenarios', 'Metrics', 'Repeats'] + const rows = + portfolios.length > 0 + ? portfolios.map((portfolio) => [ + { + title: ( + <Link href={`/projects/${portfolio.projectId}/portfolios/${portfolio._id}`}> + {portfolio.name} + </Link> + ), + }, + + portfolio.scenarioIds.length === 1 ? '1 scenario' : `${portfolio.scenarioIds.length} scenarios`, + + portfolio.targets.enabledMetrics.length === 1 + ? '1 metric' + : `${portfolio.targets.enabledMetrics.length} metrics`, + portfolio.targets.repeatsPerScenario === 1 + ? '1 repeat' + : `${portfolio.targets.repeatsPerScenario} repeats`, + ]) + : [ + { + heightAuto: true, + cells: [ + { + props: { colSpan: 4 }, + title: ( + <TableEmptyState + status={status} + loadingTitle="Loading portfolios" + emptyTitle="No portfolios" + emptyText="You have not created any portfolio for this project yet. Click the New Portfolio button to create one." + /> + ), + }, + ], + }, + ] + + const actions = + portfolios.length > 0 + ? [ + { + title: 'Delete Portfolio', + onClick: (_, rowId) => deletePortfolio(portfolios[rowId]._id), + }, + ] + : [] + + return ( + <Table aria-label="Portfolio List" variant="compact" cells={columns} rows={rows} actions={actions}> + <TableHeader /> + <TableBody /> + </Table> + ) +} + +PortfolioTable.propTypes = { + projectId: PropTypes.string, +} + +export default PortfolioTable diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js deleted file mode 100644 index 4adf3205..00000000 --- a/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js +++ /dev/null @@ -1,38 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import Link from 'next/link' -import { Button } from 'reactstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPlay, faUsers, faTrash } from '@fortawesome/free-solid-svg-icons' - -const ProjectActionButtons = ({ projectId, onViewUsers, onDelete }) => ( - <td className="text-right"> - <Link href={`/projects/${projectId}`} passHref> - <Button color="primary" outline size="sm" className="mr-2" title="Open this project"> - <FontAwesomeIcon icon={faPlay} /> - </Button> - </Link> - <Button - color="success" - outline - size="sm" - disabled - className="mr-2" - title="View and edit collaborators (not supported currently)" - onClick={() => onViewUsers(projectId)} - > - <FontAwesomeIcon icon={faUsers} /> - </Button> - <Button color="danger" outline size="sm" title="Delete this project" onClick={() => onDelete(projectId)}> - <FontAwesomeIcon icon={faTrash} /> - </Button> - </td> -) - -ProjectActionButtons.propTypes = { - projectId: PropTypes.string.isRequired, - onViewUsers: PropTypes.func, - onDelete: PropTypes.func, -} - -export default ProjectActionButtons diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js deleted file mode 100644 index 46ef4691..00000000 --- a/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js +++ /dev/null @@ -1,41 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { Project } from '../../shapes' -import ProjectRow from './ProjectRow' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons' - -const ProjectList = ({ projects }) => { - return ( - <div className="vertically-expanding-container"> - {projects.length === 0 ? ( - <div className="alert alert-info"> - <FontAwesomeIcon icon={faQuestionCircle} className="info-icon mr-2" /> - <strong>No projects here yet...</strong> Add some with the ‘New Project’ button! - </div> - ) : ( - <table className="table table-striped"> - <thead> - <tr> - <th>Project name</th> - <th>Last edited</th> - <th>Access rights</th> - <th /> - </tr> - </thead> - <tbody> - {projects.map((project) => ( - <ProjectRow project={project} key={project._id} /> - ))} - </tbody> - </table> - )} - </div> - ) -} - -ProjectList.propTypes = { - projects: PropTypes.arrayOf(Project).isRequired, -} - -export default ProjectList diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js deleted file mode 100644 index 91368de8..00000000 --- a/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react' -import ProjectActions from '../../containers/projects/ProjectActions' -import { Project } from '../../shapes' -import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from '../../util/authorizations' -import { parseAndFormatDateTime } from '../../util/date-time' -import { useAuth } from '../../auth' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' - -const ProjectRow = ({ project }) => { - const { user } = useAuth() - const { level } = project.authorizations.find((auth) => auth.userId === user.sub) - return ( - <tr> - <td className="pt-3">{project.name}</td> - <td className="pt-3">{parseAndFormatDateTime(project.datetimeLastEdited)}</td> - <td className="pt-3"> - <FontAwesomeIcon icon={AUTH_ICON_MAP[level]} className="mr-2" /> - {AUTH_DESCRIPTION_MAP[level]} - </td> - <ProjectActions projectId={project._id} /> - </tr> - ) -} - -ProjectRow.propTypes = { - project: Project.isRequired, -} - -export default ProjectRow diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js new file mode 100644 index 00000000..a7290259 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js @@ -0,0 +1,76 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Link from 'next/link' +import { Project, Status } from '../../shapes' +import { Table, TableBody, TableHeader } from '@patternfly/react-table' +import { parseAndFormatDateTime } from '../../util/date-time' +import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from '../../util/authorizations' +import { useAuth } from '../../auth' +import TableEmptyState from '../util/TableEmptyState' + +const ProjectTable = ({ status, projects, onDelete, isFiltering }) => { + const { user } = useAuth() + const columns = ['Project name', 'Last edited', 'Access Rights'] + const rows = + projects.length > 0 + ? projects.map((project) => { + const { level } = project.authorizations.find((auth) => auth.userId === user.sub) + const Icon = AUTH_ICON_MAP[level] + return [ + { + title: <Link href={`/projects/${project._id}`}>{project.name}</Link>, + }, + parseAndFormatDateTime(project.datetimeLastEdited), + { + title: ( + <> + <Icon className="pf-u-mr-md" key="auth" /> {AUTH_DESCRIPTION_MAP[level]} + </> + ), + }, + ] + }) + : [ + { + heightAuto: true, + cells: [ + { + props: { colSpan: 3 }, + title: ( + <TableEmptyState + status={status} + loadingTitle="Loading Projects" + isFiltering={isFiltering} + /> + ), + }, + ], + }, + ] + + const actions = + projects.length > 0 + ? [ + { + title: 'Delete Project', + onClick: (_, rowId) => onDelete(projects[rowId]), + }, + ] + : [] + + return ( + <Table aria-label="Project List" variant="compact" cells={columns} rows={rows} actions={actions}> + <TableHeader /> + <TableBody /> + </Table> + ) +} + +ProjectTable.propTypes = { + status: Status.isRequired, + isFiltering: PropTypes.bool, + projects: PropTypes.arrayOf(Project).isRequired, + onDelete: PropTypes.func, +} + +export default ProjectTable diff --git a/opendc-web/opendc-web-ui/src/components/projects/ScenarioState.js b/opendc-web/opendc-web-ui/src/components/projects/ScenarioState.js new file mode 100644 index 00000000..285345e7 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/ScenarioState.js @@ -0,0 +1,62 @@ +/* + * 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 { ClockIcon, InfoIcon, CheckCircleIcon, ErrorCircleOIcon } from '@patternfly/react-icons' + +function ScenarioState({ state }) { + switch (state) { + case 'CLAIMED': + case 'QUEUED': + return ( + <span> + <ClockIcon color="blue" /> Queued + </span> + ) + case 'RUNNING': + return ( + <span> + <ClockIcon color="green" /> Running + </span> + ) + case 'FINISHED': + return ( + <span> + <CheckCircleIcon color="green" /> Finished + </span> + ) + case 'FAILED': + return ( + <span> + <ErrorCircleOIcon color="red" /> Failed + </span> + ) + } + + return 'Unknown' +} + +ScenarioState.propTypes = { + state: PropTypes.oneOf(['QUEUED', 'CLAIMED', 'RUNNING', 'FINISHED', 'FAILED']), +} + +export default ScenarioState diff --git a/opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js b/opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js new file mode 100644 index 00000000..9966e3ba --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js @@ -0,0 +1,108 @@ +/* + * 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 Link from 'next/link' +import { Table, TableBody, TableHeader } from '@patternfly/react-table' +import React from 'react' +import TableEmptyState from '../util/TableEmptyState' +import ScenarioState from './ScenarioState' +import { usePortfolio, usePortfolioScenarios } from '../../data/project' +import { useProjectTopologies } from '../../data/topology' +import { useMutation } from 'react-query' + +const ScenarioTable = ({ portfolioId }) => { + const { data: portfolio } = usePortfolio(portfolioId) + const { status, data: scenarios = [] } = usePortfolioScenarios(portfolioId) + const { data: topologies } = useProjectTopologies(portfolio?.projectId, { + select: (topologies) => new Map(topologies.map((topology) => [topology._id, topology])), + }) + + const { mutate: deleteScenario } = useMutation('deleteScenario') + + const columns = ['Name', 'Topology', 'Trace', 'State'] + const rows = + scenarios.length > 0 + ? scenarios.map((scenario) => { + const topology = topologies?.get(scenario.topology.topologyId) + + return [ + scenario.name, + { + title: topology ? ( + <Link href={`/projects/${topology.projectId}/topologies/${topology._id}`}> + <a>{topology.name}</a> + </Link> + ) : ( + 'Unknown Topology' + ), + }, + scenario.trace.traceId, + { title: <ScenarioState state={scenario.simulation.state} /> }, + ] + }) + : [ + { + heightAuto: true, + cells: [ + { + props: { colSpan: 4 }, + title: ( + <TableEmptyState + status={status} + loadingTitle="Loading Scenarios" + emptyTitle="No scenarios" + emptyText="You have not created any scenario for this portfolio yet. Click the New Scenario button to create one." + /> + ), + }, + ], + }, + ] + + const actionResolver = (_, { rowIndex }) => [ + { + title: 'Delete Scenario', + onClick: (_, rowId) => deleteScenario(scenarios[rowId]._id), + isDisabled: rowIndex === 0, + }, + ] + + return ( + <Table + aria-label="Scenario List" + variant="compact" + cells={columns} + rows={rows} + actionResolver={scenarios.length > 0 ? actionResolver : undefined} + > + <TableHeader /> + <TableBody /> + </Table> + ) +} + +ScenarioTable.propTypes = { + portfolioId: PropTypes.string, +} + +export default ScenarioTable diff --git a/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js new file mode 100644 index 00000000..80099ece --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.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 PropTypes from 'prop-types' +import Link from 'next/link' +import { Table, TableBody, TableHeader } from '@patternfly/react-table' +import React from 'react' +import TableEmptyState from '../util/TableEmptyState' +import { parseAndFormatDateTime } from '../../util/date-time' +import { useMutation } from 'react-query' +import { useProjectTopologies } from '../../data/topology' + +const TopologyTable = ({ projectId }) => { + const { status, data: topologies = [] } = useProjectTopologies(projectId) + const { mutate: deleteTopology } = useMutation('deleteTopology') + + const columns = ['Name', 'Rooms', 'Last Edited'] + const rows = + topologies.length > 0 + ? topologies.map((topology) => [ + { + title: ( + <Link href={`/projects/${topology.projectId}/topologies/${topology._id}`}> + {topology.name} + </Link> + ), + }, + topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`, + parseAndFormatDateTime(topology.datetimeLastEdited), + ]) + : [ + { + heightAuto: true, + cells: [ + { + props: { colSpan: 3 }, + title: ( + <TableEmptyState + status={status} + loadingTitle="Loading topologies" + emptyTitle="No topologies" + emptyText="You have not created any topology for this project yet. Click the New Topology button to create one." + /> + ), + }, + ], + }, + ] + + const actionResolver = (_, { rowIndex }) => [ + { + title: 'Delete Topology', + onClick: (_, rowId) => deleteTopology(topologies[rowId]._id), + isDisabled: rowIndex === 0, + }, + ] + + return ( + <Table + aria-label="Topology List" + variant="compact" + cells={columns} + rows={rows} + actionResolver={topologies.length > 0 ? actionResolver : () => []} + > + <TableHeader /> + <TableBody /> + </Table> + ) +} + +TopologyTable.propTypes = { + projectId: PropTypes.string, +} + +export default TopologyTable diff --git a/opendc-web/opendc-web-ui/src/components/util/BreadcrumbLink.js b/opendc-web/opendc-web-ui/src/components/util/BreadcrumbLink.js new file mode 100644 index 00000000..c6ab214a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/util/BreadcrumbLink.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 PropTypes from 'prop-types' +import Link from 'next/link' + +const BreadcrumbLink = ({ children, href, ...props }) => ( + <Link href={href}> + <a {...props}>{children}</a> + </Link> +) + +BreadcrumbLink.propTypes = { + children: PropTypes.node, + href: PropTypes.string.isRequired, +} + +export default BreadcrumbLink diff --git a/opendc-web/opendc-web-ui/src/components/util/NavItemLink.js b/opendc-web/opendc-web-ui/src/components/util/NavItemLink.js new file mode 100644 index 00000000..c0d109bd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/util/NavItemLink.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 Link from 'next/link' +import PropTypes from 'prop-types' + +const NavItemLink = ({ children, href, ...props }) => ( + <Link href={href}> + <a {...props}>{children}</a> + </Link> +) + +NavItemLink.propTypes = { + children: PropTypes.node, + href: PropTypes.string.isRequired, +} + +export default NavItemLink diff --git a/opendc-web/opendc-web-ui/src/components/util/TableEmptyState.js b/opendc-web/opendc-web-ui/src/components/util/TableEmptyState.js new file mode 100644 index 00000000..9d16ffbb --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/util/TableEmptyState.js @@ -0,0 +1,103 @@ +/* + * 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 { Bullseye, EmptyState, EmptyStateBody, EmptyStateIcon, Spinner, Title } from '@patternfly/react-core' +import { SearchIcon, CubesIcon } from '@patternfly/react-icons' +import { Status } from '../../shapes' + +function TableEmptyState({ + status, + isFiltering, + loadingTitle = 'Loading', + emptyTitle = 'No results found', + emptyText = 'No results found of this type.', + emptyAction = '', +}) { + if (status === 'loading') { + return ( + <Bullseye> + <EmptyState variant="xs"> + <EmptyStateIcon variant="container" component={Spinner} /> + <Title headingLevel="h4" size="md"> + {loadingTitle} + </Title> + </EmptyState> + </Bullseye> + ) + } else if (status === 'error') { + return ( + <EmptyState variant="xs"> + <Title headingLevel="h4" size="md"> + Unable to connect + </Title> + <EmptyStateBody> + There was an error retrieving data. Check your connection and try again. + </EmptyStateBody> + </EmptyState> + ) + } else if (status === 'idle') { + return ( + <EmptyState variant="xs"> + <EmptyStateIcon icon={CubesIcon} /> + <Title headingLevel="h4" size="md"> + {emptyTitle} + </Title> + <EmptyStateBody>No results available at this moment.</EmptyStateBody> + </EmptyState> + ) + } else if (isFiltering) { + return ( + <EmptyState variant="xs"> + <EmptyStateIcon icon={SearchIcon} /> + <Title headingLevel="h4" size="md"> + No results found + </Title> + <EmptyStateBody> + No results match this filter criteria. Remove all filters or clear all filters to show results. + </EmptyStateBody> + </EmptyState> + ) + } + + return ( + <EmptyState variant="xs"> + <EmptyStateIcon icon={CubesIcon} /> + <Title headingLevel="h4" size="md"> + {emptyTitle} + </Title> + <EmptyStateBody>{emptyText}</EmptyStateBody> + {emptyAction} + </EmptyState> + ) +} + +TableEmptyState.propTypes = { + status: Status.isRequired, + isFiltering: PropTypes.bool, + loadingTitle: PropTypes.string, + emptyTitle: PropTypes.string, + emptyText: PropTypes.string, + emptyAction: PropTypes.node, +} + +export default TableEmptyState diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js deleted file mode 100644 index bac24c8b..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import { useDispatch } from 'react-redux' -import { goDownOneInteractionLevel } from '../../../redux/actions/interaction-level' -import GrayLayer from '../../../components/app/map/elements/GrayLayer' - -const GrayContainer = () => { - const dispatch = useDispatch() - const onClick = () => dispatch(goDownOneInteractionLevel()) - return <GrayLayer onClick={onClick} /> -} - -export default GrayContainer 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 deleted file mode 100644 index 91ceb35d..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react' -import { useDispatch } from 'react-redux' -import { setMapDimensions, setMapPositionWithBoundsCheck, zoomInOnPosition } from '../../../redux/actions/map' -import MapStageComponent from '../../../components/app/map/MapStageComponent' -import { useMapDimensions, useMapPosition } from '../../../data/map' - -const MapStage = () => { - const position = useMapPosition() - const dimensions = useMapDimensions() - 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} - mapDimensions={dimensions} - zoomInOnPosition={zoomInOnPositionA} - setMapPositionWithBoundsCheck={setMapPositionWithBoundsCheckA} - setMapDimensions={setMapDimensionsA} - /> - ) -} - -export default MapStage diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js deleted file mode 100644 index e5af5117..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' -import RackGroup from '../../../components/app/map/groups/RackGroup' - -const RackContainer = ({ tile }) => { - const interactionLevel = useSelector((state) => state.interactionLevel) - return <RackGroup interactionLeve={interactionLevel} tile={tile} /> -} - -export default RackContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js deleted file mode 100644 index 8d6f61e0..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' -import RackFillBar from '../../../components/app/map/elements/RackFillBar' - -const RackSpaceFillContainer = (props) => { - const state = useSelector((state) => { - const machineIds = state.objects.rack[state.objects.tile[props.tileId].rack].machines - return { - type: 'space', - fillFraction: machineIds.filter((id) => id !== null).length / machineIds.length, - } - }) - return <RackFillBar {...props} {...state} /> -} - -export default RackSpaceFillContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js deleted file mode 100644 index 0a9e1503..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js +++ /dev/null @@ -1,23 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { goFromBuildingToRoom } from '../../../redux/actions/interaction-level' -import RoomGroup from '../../../components/app/map/groups/RoomGroup' - -const RoomContainer = (props) => { - const state = useSelector((state) => { - return { - interactionLevel: state.interactionLevel, - currentRoomInConstruction: state.construction.currentRoomInConstruction, - room: state.objects.room[props.roomId], - } - }) - const dispatch = useDispatch() - return <RoomGroup {...props} {...state} onClick={() => dispatch(goFromBuildingToRoom(props.roomId))} /> -} - -RoomContainer.propTypes = { - roomId: PropTypes.string, -} - -export default RoomContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js deleted file mode 100644 index 50a2abfd..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { goFromRoomToRack } from '../../../redux/actions/interaction-level' -import TileGroup from '../../../components/app/map/groups/TileGroup' - -const TileContainer = (props) => { - const interactionLevel = useSelector((state) => state.interactionLevel) - const tile = useSelector((state) => state.objects.tile[props.tileId]) - - const dispatch = useDispatch() - const onClick = (tile) => { - if (tile.rack) { - dispatch(goFromRoomToRack(tile._id)) - } - } - return <TileGroup {...props} onClick={onClick} tile={tile} interactionLevel={interactionLevel} /> -} - -export default TileContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js deleted file mode 100644 index e7ab3c72..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' -import TopologyGroup from '../../../components/app/map/groups/TopologyGroup' -import { useActiveTopology } from '../../../data/topology' - -const TopologyContainer = () => { - const topology = useActiveTopology() - const interactionLevel = useSelector((state) => state.interactionLevel) - - return <TopologyGroup topology={topology} interactionLevel={interactionLevel} /> -} - -export default TopologyContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js deleted file mode 100644 index 67f36396..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' -import WallGroup from '../../../components/app/map/groups/WallGroup' - -const WallContainer = (props) => { - const tiles = useSelector((state) => - state.objects.room[props.roomId].tiles.map((tileId) => state.objects.tile[tileId]) - ) - return <WallGroup {...props} tiles={tiles} /> -} - -export default WallContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js deleted file mode 100644 index a10eea22..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import ScaleIndicatorComponent from '../../../../components/app/map/controls/ScaleIndicatorComponent' -import { useMapScale } from '../../../../data/map' - -const ScaleIndicatorContainer = (props) => { - const scale = useMapScale() - return <ScaleIndicatorComponent {...props} scale={scale} /> -} - -export default ScaleIndicatorContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js deleted file mode 100644 index a39c6077..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import { useDispatch } from 'react-redux' -import { zoomInOnCenter } from '../../../../redux/actions/map' -import ZoomControlComponent from '../../../../components/app/map/controls/ZoomControlComponent' -import { useMapScale } from '../../../../data/map' - -const ZoomControlContainer = () => { - const dispatch = useDispatch() - const scale = useMapScale() - return <ZoomControlComponent mapScale={scale} zoomInOnCenter={(zoomIn) => dispatch(zoomInOnCenter(zoomIn))} /> -} - -export default ZoomControlContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js b/opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js deleted file mode 100644 index 633ebcc7..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import MapLayerComponent from '../../../../components/app/map/layers/MapLayerComponent' -import { useMapPosition, useMapScale } from '../../../../data/map' - -const MapLayer = (props) => { - const position = useMapPosition() - const scale = useMapScale() - return <MapLayerComponent {...props} mapPosition={position} mapScale={scale} /> -} - -export default MapLayer diff --git a/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js b/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js deleted file mode 100644 index a75f15ae..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' -import PortfolioResultsComponent from '../../../components/app/results/PortfolioResultsComponent' -import { useRouter } from 'next/router' -import { usePortfolio, usePortfolioScenarios } from '../../../data/project' - -const PortfolioResultsContainer = (props) => { - const router = useRouter() - const { portfolio: currentPortfolioId } = router.query - const { data: portfolio } = usePortfolio(currentPortfolioId) - const scenarios = usePortfolioScenarios(currentPortfolioId).data ?? [] - return <PortfolioResultsComponent {...props} scenarios={scenarios} portfolio={portfolio} /> -} - -export default PortfolioResultsContainer 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 deleted file mode 100644 index 60ac666c..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useState } from 'react' -import { useRouter } from 'next/router' -import PortfolioListComponent from '../../../../components/app/sidebars/project/PortfolioListComponent' -import NewPortfolioModalComponent from '../../../../components/modals/custom-components/NewPortfolioModalComponent' -import { useProjectPortfolios } from '../../../../data/project' -import { useMutation } from 'react-query' - -const PortfolioListContainer = () => { - const router = useRouter() - const { project: currentProjectId, portfolio: currentPortfolioId } = router.query - const portfolios = useProjectPortfolios(currentProjectId).data ?? [] - - const { mutate: addPortfolio } = useMutation('addPortfolio') - const { mutateAsync: deletePortfolio } = useMutation('deletePortfolio') - - const [isVisible, setVisible] = useState(false) - const actions = { - onNewPortfolio: () => setVisible(true), - onChoosePortfolio: async (portfolioId) => { - await router.push(`/projects/${currentProjectId}/portfolios/${portfolioId}`) - }, - onDeletePortfolio: async (id) => { - if (id) { - await deletePortfolio(id) - await router.push(`/projects/${currentProjectId}`) - } - }, - } - const callback = (name, targets) => { - if (name) { - addPortfolio({ projectId: currentProjectId, name, targets }) - } - setVisible(false) - } - return ( - <> - <PortfolioListComponent - currentProjectId={currentProjectId} - currentPortfolioId={currentPortfolioId} - portfolios={portfolios} - {...actions} - /> - <NewPortfolioModalComponent callback={callback} show={isVisible} /> - </> - ) -} - -export default PortfolioListContainer 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 deleted file mode 100644 index 06c7f0f7..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import { useRouter } from 'next/router' -import ProjectSidebarComponent from '../../../../components/app/sidebars/project/ProjectSidebarComponent' -import { isCollapsible } from '../../../../util/sidebar-space' - -const ProjectSidebarContainer = (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/ScenarioListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js deleted file mode 100644 index 3b68df38..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types' -import React, { useState } from 'react' -import ScenarioListComponent from '../../../../components/app/sidebars/project/ScenarioListComponent' -import NewScenarioModalComponent from '../../../../components/modals/custom-components/NewScenarioModalComponent' -import { useProjectTopologies } from '../../../../data/topology' -import { usePortfolio, usePortfolioScenarios } from '../../../../data/project' -import { useSchedulers, useTraces } from '../../../../data/experiments' -import { useMutation } from 'react-query' - -const ScenarioListContainer = ({ portfolioId }) => { - const { data: portfolio } = usePortfolio(portfolioId) - const scenarios = usePortfolioScenarios(portfolioId).data ?? [] - const topologies = - useProjectTopologies(portfolio?.projectId).data?.map((topology) => ({ - _id: topology._id, - name: topology.name, - })) ?? [] - const traces = useTraces().data ?? [] - const schedulers = useSchedulers().data ?? [] - - const { mutate: addScenario } = useMutation('addScenario') - const { mutate: deleteScenario } = useMutation('deleteScenario') - - const [isVisible, setVisible] = useState(false) - - const onNewScenario = () => setVisible(true) - const onDeleteScenario = (id) => id && deleteScenario(id) - const callback = (name, portfolioId, trace, topology, operational) => { - if (name) { - addScenario({ - portfolioId, - name, - trace, - topology, - operational, - }) - } - - setVisible(false) - } - - return ( - <> - <ScenarioListComponent - portfolioId={portfolioId} - scenarios={scenarios} - onNewScenario={onNewScenario} - onDeleteScenario={onDeleteScenario} - /> - <NewScenarioModalComponent - show={isVisible} - currentPortfolioId={portfolioId} - currentPortfolioScenarioIds={scenarios.map((s) => s._id)} - traces={traces} - schedulers={schedulers} - topologies={topologies} - callback={callback} - /> - </> - ) -} - -ScenarioListContainer.propTypes = { - portfolioId: PropTypes.string, -} - -export default ScenarioListContainer 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 deleted file mode 100644 index a2244a30..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useState } from 'react' -import { useDispatch } from 'react-redux' -import TopologyListComponent from '../../../../components/app/sidebars/project/TopologyListComponent' -import { setCurrentTopology } from '../../../../redux/actions/topology/building' -import { useRouter } from 'next/router' -import { addTopology } from '../../../../redux/actions/topologies' -import NewTopologyModalComponent from '../../../../components/modals/custom-components/NewTopologyModalComponent' -import { useActiveTopology, useProjectTopologies } from '../../../../data/topology' -import { useMutation } from 'react-query' - -const TopologyListContainer = () => { - const dispatch = useDispatch() - const router = useRouter() - const { project: currentProjectId } = router.query - const topologies = - useProjectTopologies(currentProjectId).data?.map((topology) => ({ _id: topology._id, name: topology.name })) ?? - [] - const currentTopologyId = useActiveTopology()?._id - const [isVisible, setVisible] = useState(false) - - const { mutate: deleteTopology } = useMutation('deleteTopology') - - const onChooseTopology = async (id) => { - dispatch(setCurrentTopology(id)) - await router.push(`/projects/${currentProjectId}/topologies/${id}`) - } - const onDeleteTopology = async (id) => { - if (id) { - deleteTopology(id) - await router.push(`/projects/${currentProjectId}`) - } - } - const onCreateTopology = (name) => { - if (name) { - dispatch(addTopology(currentProjectId, name, undefined)) - } - setVisible(false) - } - const onDuplicateTopology = (name, id) => { - if (name) { - dispatch(addTopology(currentProjectId, name, id)) - } - setVisible(false) - } - const onCancel = () => setVisible(false) - - return ( - <> - <TopologyListComponent - topologies={topologies} - currentTopologyId={currentTopologyId} - onChooseTopology={onChooseTopology} - onNewTopology={() => setVisible(true)} - onDeleteTopology={onDeleteTopology} - /> - <NewTopologyModalComponent - show={isVisible} - topologies={topologies} - onCreateTopology={onCreateTopology} - onDuplicateTopology={onDuplicateTopology} - onCancel={onCancel} - /> - </> - ) -} - -export default TopologyListContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js deleted file mode 100644 index 42c81c65..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' -import TopologySidebarComponent from '../../../../components/app/sidebars/topology/TopologySidebarComponent' - -const TopologySidebarContainer = (props) => { - const interactionLevel = useSelector((state) => state.interactionLevel) - return <TopologySidebarComponent {...props} interactionLevel={interactionLevel} /> -} - -export default TopologySidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js deleted file mode 100644 index a0b52e56..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js +++ /dev/null @@ -1,5 +0,0 @@ -import BuildingSidebarComponent from '../../../../../components/app/sidebars/topology/building/BuildingSidebarComponent' - -const BuildingSidebarContainer = BuildingSidebarComponent - -export default BuildingSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js deleted file mode 100644 index 96f42a44..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { - cancelNewRoomConstruction, - finishNewRoomConstruction, - startNewRoomConstruction, -} from '../../../../../redux/actions/topology/building' -import StartNewRoomConstructionComponent from '../../../../../components/app/sidebars/topology/building/NewRoomConstructionComponent' - -const NewRoomConstructionButton = (props) => { - const currentRoomInConstruction = useSelector((state) => state.construction.currentRoomInConstruction) - - const dispatch = useDispatch() - const actions = { - onStart: () => dispatch(startNewRoomConstruction()), - onFinish: () => dispatch(finishNewRoomConstruction()), - onCancel: () => dispatch(cancelNewRoomConstruction()), - } - return ( - <StartNewRoomConstructionComponent - {...props} - {...actions} - currentRoomInConstruction={currentRoomInConstruction} - /> - ) -} - -export default NewRoomConstructionButton diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js deleted file mode 100644 index ea250767..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import { useDispatch } from 'react-redux' -import { goDownOneInteractionLevel } from '../../../../../redux/actions/interaction-level' -import BackToRackComponent from '../../../../../components/app/sidebars/topology/machine/BackToRackComponent' - -const BackToRackContainer = (props) => { - const dispatch = useDispatch() - return <BackToRackComponent {...props} onClick={() => dispatch(goDownOneInteractionLevel())} /> -} - -export default BackToRackContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js deleted file mode 100644 index 54e406f4..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useState } from 'react' -import { useDispatch } from 'react-redux' -import ConfirmationModal from '../../../../../components/modals/ConfirmationModal' -import { deleteMachine } from '../../../../../redux/actions/topology/machine' -import { Button } from 'reactstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faTrash } from '@fortawesome/free-solid-svg-icons' - -const DeleteMachineContainer = () => { - const dispatch = useDispatch() - const [isVisible, setVisible] = useState(false) - const callback = (isConfirmed) => { - if (isConfirmed) { - dispatch(deleteMachine()) - } - setVisible(false) - } - return ( - <> - <Button color="danger" outline block onClick={() => setVisible(true)}> - <FontAwesomeIcon icon={faTrash} className="mr-2" /> - Delete this machine - </Button> - <ConfirmationModal - title="Delete this machine" - message="Are you sure you want to delete this machine?" - show={isVisible} - callback={callback} - /> - </> - ) -} - -export default DeleteMachineContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js deleted file mode 100644 index 9cbb32c5..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' - -const MachineNameContainer = () => { - const position = useSelector((state) => state.interactionLevel.position) - return <h2>Machine at slot {position}</h2> -} - -export default MachineNameContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js deleted file mode 100644 index 7553c2fe..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' -import MachineSidebarComponent from '../../../../../components/app/sidebars/topology/machine/MachineSidebarComponent' - -const MachineSidebarContainer = (props) => { - const machineId = useSelector( - (state) => - state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].machines[ - state.interactionLevel.position - 1 - ] - ) - return <MachineSidebarComponent {...props} machineId={machineId} /> -} - -export default MachineSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js deleted file mode 100644 index 0f85aa76..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { addUnit } from '../../../../../redux/actions/topology/machine' -import UnitAddComponent from '../../../../../components/app/sidebars/topology/machine/UnitAddComponent' - -const UnitAddContainer = (props) => { - const units = useSelector((state) => Object.values(state.objects[props.unitType])) - const dispatch = useDispatch() - - const onAdd = (id) => dispatch(addUnit(props.unitType, id)) - - return <UnitAddComponent {...props} onAdd={onAdd} units={units} /> -} - -export default UnitAddContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js deleted file mode 100644 index cdd7e268..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import UnitListComponent from '../../../../../components/app/sidebars/topology/machine/UnitListComponent' -import { deleteUnit } from '../../../../../redux/actions/topology/machine' - -const unitMapping = { - cpu: 'cpus', - gpu: 'gpus', - memory: 'memories', - storage: 'storages', -} - -const UnitListContainer = ({ unitType, ...props }) => { - const dispatch = useDispatch() - const units = useSelector((state) => { - const machine = - state.objects.machine[ - state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].machines[ - state.interactionLevel.position - 1 - ] - ] - return machine[unitMapping[unitType]].map((id) => state.objects[unitType][id]) - }) - const onDelete = (unit, unitType) => dispatch(deleteUnit(unitType, unit._id)) - - return <UnitListComponent {...props} units={units} unitType={unitType} onDelete={onDelete} /> -} - -UnitListContainer.propTypes = { - unitType: PropTypes.string.isRequired, -} - -export default UnitListContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js deleted file mode 100644 index 00fe4067..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js +++ /dev/null @@ -1,5 +0,0 @@ -import UnitTabsComponent from '../../../../../components/app/sidebars/topology/machine/UnitTabsComponent' - -const UnitTabsContainer = UnitTabsComponent - -export default UnitTabsContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js deleted file mode 100644 index c2a0fc48..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import { useDispatch } from 'react-redux' -import { addPrefab } from '../../../../../redux/actions/prefabs' -import AddPrefabComponent from '../../../../../components/app/sidebars/topology/rack/AddPrefabComponent' - -const AddPrefabContainer = (props) => { - const dispatch = useDispatch() - return <AddPrefabComponent {...props} onClick={() => dispatch(addPrefab('name'))} /> -} - -export default AddPrefabContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js deleted file mode 100644 index a98728a6..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import { useDispatch } from 'react-redux' -import { goDownOneInteractionLevel } from '../../../../../redux/actions/interaction-level' -import BackToRoomComponent from '../../../../../components/app/sidebars/topology/rack/BackToRoomComponent' - -const BackToRoomContainer = (props) => { - const dispatch = useDispatch() - return <BackToRoomComponent {...props} onClick={() => dispatch(goDownOneInteractionLevel())} /> -} - -export default BackToRoomContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js deleted file mode 100644 index 4463530e..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useState } from 'react' -import { useDispatch } from 'react-redux' -import ConfirmationModal from '../../../../../components/modals/ConfirmationModal' -import { deleteRack } from '../../../../../redux/actions/topology/rack' -import { Button } from 'reactstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faTrash } from '@fortawesome/free-solid-svg-icons' - -const DeleteRackContainer = () => { - const dispatch = useDispatch() - const [isVisible, setVisible] = useState(false) - const callback = (isConfirmed) => { - if (isConfirmed) { - dispatch(deleteRack()) - } - setVisible(false) - } - return ( - <> - <Button color="danger" outline block onClick={() => setVisible(true)}> - <FontAwesomeIcon icon={faTrash} className="mr-2" /> - Delete this rack - </Button> - <ConfirmationModal - title="Delete this rack" - message="Are you sure you want to delete this rack?" - show={isVisible} - callback={callback} - /> - </> - ) -} - -export default DeleteRackContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js deleted file mode 100644 index 2118d915..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useMemo } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import MachineListComponent from '../../../../../components/app/sidebars/topology/rack/MachineListComponent' -import { goFromRackToMachine } from '../../../../../redux/actions/interaction-level' -import { addMachine } from '../../../../../redux/actions/topology/rack' - -const MachineListContainer = (props) => { - const rack = useSelector((state) => state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack]) - const machines = useSelector((state) => rack.machines.map((id) => state.objects.machine[id])) - const machinesNull = useMemo(() => { - const res = Array(rack.capacity).fill(null) - for (const machine of machines) { - res[machine.position - 1] = machine - } - return res - }, [rack, machines]) - const dispatch = useDispatch() - - return ( - <MachineListComponent - {...props} - machines={machinesNull} - onAdd={(index) => dispatch(addMachine(index))} - onSelect={(index) => dispatch(goFromRackToMachine(index))} - /> - ) -} - -export default MachineListContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js deleted file mode 100644 index 2c39cf9f..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import NameComponent from '../../../../../components/app/sidebars/topology/NameComponent' -import TextInputModal from '../../../../../components/modals/TextInputModal' -import { editRackName } from '../../../../../redux/actions/topology/rack' - -const RackNameContainer = () => { - const [isVisible, setVisible] = useState(false) - const rackName = useSelector( - (state) => state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].name - ) - const dispatch = useDispatch() - const callback = (name) => { - if (name) { - dispatch(editRackName(name)) - } - setVisible(false) - } - return ( - <> - <NameComponent name={rackName} onEdit={() => setVisible(true)} /> - <TextInputModal - title="Edit rack name" - label="Rack name" - show={isVisible} - initialValue={rackName} - callback={callback} - /> - </> - ) -} - -export default RackNameContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js deleted file mode 100644 index 34777125..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' -import RackSidebarComponent from '../../../../../components/app/sidebars/topology/rack/RackSidebarComponent' - -const RackSidebarContainer = (props) => { - const rackId = useSelector((state) => state.objects.tile[state.interactionLevel.tileId].rack) - return <RackSidebarComponent {...props} rackId={rackId} /> -} - -export default RackSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js deleted file mode 100644 index 9fa1e95f..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import { useDispatch } from 'react-redux' -import { goDownOneInteractionLevel } from '../../../../../redux/actions/interaction-level' -import BackToBuildingComponent from '../../../../../components/app/sidebars/topology/room/BackToBuildingComponent' - -const BackToBuildingContainer = () => { - const dispatch = useDispatch() - const onClick = () => dispatch(goDownOneInteractionLevel()) - return <BackToBuildingComponent onClick={onClick} /> -} - -export default BackToBuildingContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js deleted file mode 100644 index 0fbbb036..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useState } from 'react' -import { useDispatch } from 'react-redux' -import { Button } from 'reactstrap' -import ConfirmationModal from '../../../../../components/modals/ConfirmationModal' -import { deleteRoom } from '../../../../../redux/actions/topology/room' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faTrash } from '@fortawesome/free-solid-svg-icons' - -const DeleteRoomContainer = () => { - const dispatch = useDispatch() - const [isVisible, setVisible] = useState(false) - const callback = (isConfirmed) => { - if (isConfirmed) { - dispatch(deleteRoom()) - } - setVisible(false) - } - return ( - <> - <Button color="danger" outline block onClick={() => setVisible(true)}> - <FontAwesomeIcon icon={faTrash} className="mr-2" /> - Delete this room - </Button> - <ConfirmationModal - title="Delete this room" - message="Are you sure you want to delete this room?" - show={isVisible} - callback={callback} - /> - </> - ) -} - -export default DeleteRoomContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js deleted file mode 100644 index ec4f586b..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { finishRoomEdit, startRoomEdit } from '../../../../../redux/actions/topology/building' -import { Button } from 'reactstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faCheck, faPencilAlt } from '@fortawesome/free-solid-svg-icons' - -const EditRoomContainer = () => { - const isEditing = useSelector((state) => state.construction.currentRoomInConstruction !== '-1') - const isInRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode) - - const dispatch = useDispatch() - const onEdit = () => dispatch(startRoomEdit()) - const onFinish = () => dispatch(finishRoomEdit()) - - return isEditing ? ( - <Button color="info" outline block onClick={onFinish}> - <FontAwesomeIcon icon={faCheck} className="mr-2" /> - Finish editing room - </Button> - ) : ( - <Button - color="info" - outline - block - disabled={isInRackConstructionMode} - onClick={() => (isInRackConstructionMode ? undefined : onEdit())} - > - <FontAwesomeIcon icon={faPencilAlt} className="mr-2" /> - Edit the tiles of this room - </Button> - ) -} - -export default EditRoomContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js deleted file mode 100644 index 79584e98..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { startRackConstruction, stopRackConstruction } from '../../../../../redux/actions/topology/room' -import RackConstructionComponent from '../../../../../components/app/sidebars/topology/room/RackConstructionComponent' - -const RackConstructionContainer = (props) => { - const isRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode) - const isEditingRoom = useSelector((state) => state.construction.currentRoomInConstruction !== '-1') - - const dispatch = useDispatch() - const onStart = () => dispatch(startRackConstruction()) - const onStop = () => dispatch(stopRackConstruction()) - return ( - <RackConstructionComponent - {...props} - inRackConstructionMode={isRackConstructionMode} - isEditingRoom={isEditingRoom} - onStart={onStart} - onStop={onStop} - /> - ) -} - -export default RackConstructionContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js deleted file mode 100644 index 3b35a849..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js +++ /dev/null @@ -1,31 +0,0 @@ -import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import NameComponent from '../../../../../components/app/sidebars/topology/NameComponent' -import TextInputModal from '../../../../../components/modals/TextInputModal' -import { editRoomName } from '../../../../../redux/actions/topology/room' - -const RoomNameContainer = () => { - const [isVisible, setVisible] = useState(false) - const roomName = useSelector((state) => state.objects.room[state.interactionLevel.roomId].name) - const dispatch = useDispatch() - const callback = (name) => { - if (name) { - dispatch(editRoomName(name)) - } - setVisible(false) - } - return ( - <> - <NameComponent name={roomName} onEdit={() => setVisible(true)} /> - <TextInputModal - title="Edit room name" - label="Room name" - show={isVisible} - initialValue={roomName} - callback={callback} - /> - </> - ) -} - -export default RoomNameContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js deleted file mode 100644 index 252881a0..00000000 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' -import RoomSidebarComponent from '../../../../../components/app/sidebars/topology/room/RoomSidebarComponent' - -const RoomSidebarContainer = (props) => { - const roomId = useSelector((state) => state.interactionLevel.roomId) - return <RoomSidebarComponent {...props} roomId={roomId} /> -} - -export default RoomSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/auth/Login.js b/opendc-web/opendc-web-ui/src/containers/auth/Login.js deleted file mode 100644 index d8083d89..00000000 --- a/opendc-web/opendc-web-ui/src/containers/auth/Login.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import { Button } from 'reactstrap' -import { useAuth } from '../../auth' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faSignInAlt } from '@fortawesome/free-solid-svg-icons' - -function Login({ visible, className }) { - const { loginWithRedirect } = useAuth() - - if (!visible) { - return <span /> - } - - return ( - <Button color="primary" onClick={() => loginWithRedirect()} className={className}> - <FontAwesomeIcon icon={faSignInAlt} /> Sign In - </Button> - ) -} - -export default Login diff --git a/opendc-web/opendc-web-ui/src/containers/auth/Logout.js b/opendc-web/opendc-web-ui/src/containers/auth/Logout.js deleted file mode 100644 index 37705c5d..00000000 --- a/opendc-web/opendc-web-ui/src/containers/auth/Logout.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import LogoutButton from '../../components/navigation/LogoutButton' -import { useAuth } from '../../auth' - -const Logout = (props) => { - const { logout } = useAuth() - return <LogoutButton {...props} onLogout={() => logout({ returnTo: window.location.origin })} /> -} - -export default Logout diff --git a/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js b/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js deleted file mode 100644 index 70f5b884..00000000 --- a/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' -import { useAuth } from '../../auth' - -function ProfileName() { - const { isLoading, user } = useAuth() - return isLoading ? <span>Loading...</span> : <span>{user.name}</span> -} - -export default ProfileName diff --git a/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js b/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js deleted file mode 100644 index ff9f9fe7..00000000 --- a/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import AppNavbarComponent from '../../components/navigation/AppNavbarComponent' -import { useActiveProjectId, useProject } from '../../data/project' - -const AppNavbarContainer = (props) => { - const projectId = useActiveProjectId() - const { data: project } = useProject(projectId) - return <AppNavbarComponent {...props} project={project} /> -} - -export default AppNavbarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js b/opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js deleted file mode 100644 index ac0edae4..00000000 --- a/opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useState } from 'react' -import TextInputModal from '../../components/modals/TextInputModal' -import { Button } from 'reactstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPlus } from '@fortawesome/free-solid-svg-icons' -import { useMutation } from 'react-query' - -/** - * A container for creating a new project. - */ -const NewProjectContainer = () => { - const [isVisible, setVisible] = useState(false) - const { mutate: addProject } = useMutation('addProject') - const callback = (text) => { - if (text) { - addProject({ name: text }) - } - setVisible(false) - } - - return ( - <> - <div className="bottom-btn-container"> - <Button color="primary" className="float-right" onClick={() => setVisible(true)}> - <FontAwesomeIcon icon={faPlus} className="mr-2" /> - New Project - </Button> - </div> - <TextInputModal title="New Project" label="Project title" show={isVisible} callback={callback} /> - </> - ) -} - -export default NewProjectContainer diff --git a/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js b/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js deleted file mode 100644 index 62985742..00000000 --- a/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' -import ProjectActionButtons from '../../components/projects/ProjectActionButtons' -import { useMutation } from 'react-query' - -const ProjectActions = (props) => { - const { mutate: deleteProject } = useMutation('deleteProject') - const actions = { - onViewUsers: (id) => {}, // TODO implement user viewing - onDelete: (id) => deleteProject(id), - } - return <ProjectActionButtons {...props} {...actions} /> -} - -export default ProjectActions diff --git a/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js b/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js deleted file mode 100644 index b5c5dd68..00000000 --- a/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import ProjectList from '../../components/projects/ProjectList' -import { useAuth } from '../../auth' -import { useProjects } from '../../data/project' - -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 - } -} - -const ProjectListContainer = ({ filter }) => { - const { user } = useAuth() - const { data: projects } = useProjects() - return <ProjectList projects={getVisibleProjects(projects ?? [], filter, user?.sub)} /> -} - -ProjectListContainer.propTypes = { - filter: PropTypes.string.isRequired, -} - -export default ProjectListContainer diff --git a/opendc-web/opendc-web-ui/src/data/map.js b/opendc-web/opendc-web-ui/src/data/map.js index 6aef6ac5..348a6664 100644 --- a/opendc-web/opendc-web-ui/src/data/map.js +++ b/opendc-web/opendc-web-ui/src/data/map.js @@ -35,7 +35,3 @@ export function useMapScale() { export function useMapPosition() { return useSelector((state) => state.map.position) } - -export function useMapDimensions() { - return useSelector((state) => state.map.dimensions) -} diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index 9bdcfb93..9dcd8532 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -20,9 +20,8 @@ * SOFTWARE. */ -import { useQueries, useQuery } from 'react-query' +import { useQuery } from 'react-query' import { addProject, deleteProject, fetchProject, fetchProjects } from '../api/projects' -import { useRouter } from 'next/router' import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfoliosOfProject } from '../api/portfolios' import { addScenario, deleteScenario, fetchScenario, fetchScenariosOfPortfolio } from '../api/scenarios' @@ -38,6 +37,7 @@ export function configureProjectClient(queryClient, auth) { mutationFn: (data) => addProject(auth, data), onSuccess: async (result) => { queryClient.setQueryData('projects', (old = []) => [...old, result]) + queryClient.setQueryData(['projects', result._id], result) }, }) queryClient.setMutationDefaults('deleteProject', { @@ -61,6 +61,7 @@ export function configureProjectClient(queryClient, auth) { ...old, portfolioIds: [...old.portfolioIds, result._id], })) + queryClient.setQueryData(['project-portfolios', result.projectId], (old = []) => [...old, result]) queryClient.setQueryData(['portfolios', result._id], result) }, }) @@ -71,6 +72,9 @@ export function configureProjectClient(queryClient, auth) { ...old, portfolioIds: old.portfolioIds.filter((id) => id !== result._id), })) + queryClient.setQueryData(['project-portfolios', result.projectId], (old = []) => + old.filter((portfolio) => portfolio._id !== result._id) + ) queryClient.removeQueries(['portfolios', result._id]) }, }) @@ -86,6 +90,7 @@ export function configureProjectClient(queryClient, auth) { onSuccess: async (result) => { // Register updated scenario in cache queryClient.setQueryData(['scenarios', result._id], result) + queryClient.setQueryData(['portfolio-scenarios', result.portfolioId], (old = []) => [...old, result]) // Add scenario id to portfolio queryClient.setQueryData(['portfolios', result.portfolioId], (old) => ({ @@ -101,6 +106,9 @@ export function configureProjectClient(queryClient, auth) { ...old, scenarioIds: old.scenarioIds.filter((id) => id !== result._id), })) + queryClient.setQueryData(['portfolio-scenarios', result.portfolioId], (old = []) => + old.filter((scenario) => scenario._id !== result._id) + ) queryClient.removeQueries(['scenarios', result._id]) }, }) @@ -109,54 +117,34 @@ export function configureProjectClient(queryClient, auth) { /** * Return the available projects. */ -export function useProjects() { - return useQuery('projects') +export function useProjects(options = {}) { + return useQuery('projects', options) } /** * Return the project with the specified identifier. */ -export function useProject(projectId) { - return useQuery(['projects', projectId], { enabled: !!projectId }) +export function useProject(projectId, options = {}) { + return useQuery(['projects', projectId], { enabled: !!projectId, ...options }) } /** * Return the portfolio with the specified identifier. */ -export function usePortfolio(portfolioId) { - return useQuery(['portfolios', portfolioId], { enabled: !!portfolioId }) +export function usePortfolio(portfolioId, options = {}) { + return useQuery(['portfolios', portfolioId], { enabled: !!portfolioId, ...options }) } /** * Return the portfolios of the specified project. */ -export function useProjectPortfolios(projectId) { - return useQuery(['project-portfolios', projectId], { enabled: !!projectId }) -} - -/** - * Return the scenarios with the specified identifiers. - */ -export function useScenarios(scenarioIds) { - return useQueries( - scenarioIds.map((scenarioId) => ({ - queryKey: ['scenarios', scenarioId], - })) - ) +export function useProjectPortfolios(projectId, options = {}) { + return useQuery(['project-portfolios', projectId], { enabled: !!projectId, ...options }) } /** * Return the scenarios of the specified portfolio. */ -export function usePortfolioScenarios(portfolioId) { - return useQuery(['portfolio-scenarios', portfolioId], { enabled: !!portfolioId }) -} - -/** - * Return the current active project identifier. - */ -export function useActiveProjectId() { - const router = useRouter() - const { project } = router.query - return project +export function usePortfolioScenarios(portfolioId, options = {}) { + return useQuery(['portfolio-scenarios', portfolioId], { enabled: !!portfolioId, ...options }) } diff --git a/opendc-web/opendc-web-ui/src/data/topology.js b/opendc-web/opendc-web-ui/src/data/topology.js index 8db75877..14bd7562 100644 --- a/opendc-web/opendc-web-ui/src/data/topology.js +++ b/opendc-web/opendc-web-ui/src/data/topology.js @@ -21,7 +21,7 @@ */ import { useSelector } from 'react-redux' -import { useQueries, useQuery } from 'react-query' +import { useQuery } from 'react-query' import { addTopology, deleteTopology, fetchTopologiesOfProject, fetchTopology, updateTopology } from '../api/topologies' /** @@ -40,6 +40,7 @@ export function configureTopologyClient(queryClient, auth) { ...old, topologyIds: [...old.topologyIds, result._id], })) + queryClient.setQueryData(['project-topologies', result.projectId], (old = []) => [...old, result]) queryClient.setQueryData(['topologies', result._id], result) }, }) @@ -54,6 +55,9 @@ export function configureTopologyClient(queryClient, auth) { ...old, topologyIds: old.topologyIds.filter((id) => id !== result._id), })) + queryClient.setQueryData(['project-topologies', result.projectId], (old = []) => + old.filter((topology) => topology._id !== result._id) + ) queryClient.removeQueries(['topologies', result._id]) }, }) @@ -69,6 +73,6 @@ export function useActiveTopology() { /** * Return the topologies of the specified project. */ -export function useProjectTopologies(projectId) { - return useQuery(['project-topologies', projectId], { enabled: !!projectId }) +export function useProjectTopologies(projectId, options = {}) { + return useQuery(['project-topologies', projectId], { enabled: !!projectId, ...options }) } diff --git a/opendc-web/opendc-web-ui/src/index.scss b/opendc-web/opendc-web-ui/src/index.scss deleted file mode 100644 index dbd9550c..00000000 --- a/opendc-web/opendc-web-ui/src/index.scss +++ /dev/null @@ -1,68 +0,0 @@ -@import '~bootstrap/scss/bootstrap'; - -@import './style/_mixins.scss'; -@import './style/_variables.scss'; - -html, -body, -#__next { - margin: 0; - padding: 0; - width: 100%; - height: 100%; - - font-family: Roboto, Helvetica, Verdana, sans-serif; - background: #eee; - - // Scroll padding for top navbar - scroll-padding-top: 60px; -} - -.full-height { - position: relative; - height: 100% !important; -} - -.page-container { - padding-top: 60px; -} - -.text-page-container { - padding-top: 80px; - display: flex; - flex-flow: column; -} - -.vertically-expanding-container { - flex: 1 1 auto; - overflow-y: auto; -} - -.bottom-btn-container { - flex: 0 1 auto; - padding: 20px 0; -} - -.btn, -.list-group-item-action, -.clickable { - @include clickable; -} - -.btn-circle { - border-radius: 50%; -} - -a, -a:hover { - text-decoration: none; -} - -.app-page-container { - padding-left: $side-bar-width; - padding-top: 15px; -} - -.w-70 { - width: 70% !important; -} 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 78402323..028dab10 100644 --- a/opendc-web/opendc-web-ui/src/pages/_app.js +++ b/opendc-web/opendc-web-ui/src/pages/_app.js @@ -24,8 +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 '@patternfly/react-core/dist/styles/base.css' import { AuthProvider, useAuth } from '../auth' import * as Sentry from '@sentry/react' import { Integrations } from '@sentry/tracing' @@ -35,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/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> ) } diff --git a/opendc-web/opendc-web-ui/src/shapes.js b/opendc-web/opendc-web-ui/src/shapes.js index 3c27ad11..abdf146e 100644 --- a/opendc-web/opendc-web-ui/src/shapes.js +++ b/opendc-web/opendc-web-ui/src/shapes.js @@ -149,3 +149,5 @@ export const InteractionLevel = PropTypes.shape({ roomId: PropTypes.string, rackId: PropTypes.string, }) + +export const Status = PropTypes.oneOf(['idle', 'loading', 'error', 'success']) diff --git a/opendc-web/opendc-web-ui/src/style/_mixins.scss b/opendc-web/opendc-web-ui/src/style/_mixins.scss deleted file mode 100644 index 5f103cd7..00000000 --- a/opendc-web/opendc-web-ui/src/style/_mixins.scss +++ /dev/null @@ -1,5 +0,0 @@ -/* General Button Abstractions */ -@mixin clickable { - cursor: pointer; - user-select: none; -} diff --git a/opendc-web/opendc-web-ui/src/style/_variables.scss b/opendc-web/opendc-web-ui/src/style/_variables.scss deleted file mode 100644 index e3df6cbd..00000000 --- a/opendc-web/opendc-web-ui/src/style/_variables.scss +++ /dev/null @@ -1,31 +0,0 @@ -// Sizes and Margins -$document-padding: 20px; -$inter-element-margin: 5px; -$standard-border-radius: 5px; -$side-menu-width: 350px; -$color-indicator-width: 140px; - -$global-padding: 30px; -$side-bar-width: 350px; -$navbar-height: 50px; -$navbar-padding: 10px; - -// Durations -$transition-length: 150ms; - -// Colors -$gray-very-dark: #5c5c5c; -$gray-dark: #aaa; -$gray-semi-dark: #bbb; -$gray-semi-light: #ccc; -$gray-light: #ddd; -$gray-very-light: #eee; -$blue: #00a6d6; -$blue-dark: #0087b5; -$blue-very-dark: #006182; -$blue-light: #deebf7; - -// Media queries -$screen-sm: 768px; -$screen-md: 992px; -$screen-lg: 1200px; diff --git a/opendc-web/opendc-web-ui/src/style/index.scss b/opendc-web/opendc-web-ui/src/style/index.scss new file mode 100644 index 00000000..ff84e24a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/style/index.scss @@ -0,0 +1,36 @@ +/*! + * 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. + */ + +body, +#__next { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + + background: #eee; +} + +.full-height { + position: relative; + height: 100% !important; +} diff --git a/opendc-web/opendc-web-ui/src/util/authorizations.js b/opendc-web/opendc-web-ui/src/util/authorizations.js index eb492d26..ce5d34b6 100644 --- a/opendc-web/opendc-web-ui/src/util/authorizations.js +++ b/opendc-web/opendc-web-ui/src/util/authorizations.js @@ -1,9 +1,11 @@ -import { faHome, faPencilAlt, faEye } from '@fortawesome/free-solid-svg-icons' +import HomeIcon from '@patternfly/react-icons/dist/js/icons/home-icon' +import EditIcon from '@patternfly/react-icons/dist/js/icons/edit-icon' +import EyeIcon from '@patternfly/react-icons/dist/js/icons/eye-icon' export const AUTH_ICON_MAP = { - OWN: faHome, - EDIT: faPencilAlt, - VIEW: faEye, + OWN: HomeIcon, + EDIT: EditIcon, + VIEW: EyeIcon, } export const AUTH_DESCRIPTION_MAP = { diff --git a/opendc-web/opendc-web-ui/src/util/available-metrics.js b/opendc-web/opendc-web-ui/src/util/available-metrics.js index 807bc0c1..b21ab150 100644 --- a/opendc-web/opendc-web-ui/src/util/available-metrics.js +++ b/opendc-web/opendc-web-ui/src/util/available-metrics.js @@ -1,12 +1,28 @@ +export const METRIC_GROUPS = { + 'Host Metrics': [ + 'total_overcommitted_burst', + 'total_power_draw', + 'total_failure_vm_slices', + 'total_granted_burst', + 'total_interfered_burst', + 'total_requested_burst', + 'mean_cpu_usage', + 'mean_cpu_demand', + 'mean_num_deployed_images', + 'max_num_deployed_images', + ], + 'Compute Service Metrics': ['total_vms_submitted', 'total_vms_queued', 'total_vms_finished', 'total_vms_failed'], +} + export const AVAILABLE_METRICS = [ + 'mean_cpu_usage', + 'mean_cpu_demand', + 'total_requested_burst', + 'total_granted_burst', 'total_overcommitted_burst', + 'total_interfered_burst', 'total_power_draw', 'total_failure_vm_slices', - 'total_granted_burst', - 'total_interfered_burst', - 'total_requested_burst', - 'mean_cpu_usage', - 'mean_cpu_demand', 'mean_num_deployed_images', 'max_num_deployed_images', 'total_vms_submitted', @@ -65,3 +81,22 @@ export const METRIC_UNITS = { total_vms_finished: 'VMs', total_vms_failed: 'VMs', } + +export const METRIC_DESCRIPTIONS = { + total_overcommitted_burst: + 'The total CPU clock cycles lost due to overcommitting of resources. This metric is an indicator for resource overload.', + total_requested_burst: 'The total CPU clock cycles that were requested by all virtual machines.', + total_granted_burst: 'The total CPU clock cycles executed by the hosts.', + total_interfered_burst: 'The total CPU clock cycles lost due to resource interference between virtual machines.', + total_power_draw: 'The average power usage in watts.', + mean_cpu_usage: 'The average amount of CPU clock cycles consumed by all virtual machines on a host.', + mean_cpu_demand: 'The average amount of CPU clock cycles requested by all powered on virtual machines on a host.', + mean_num_deployed_images: 'The average number of virtual machines deployed on a host.', + max_num_deployed_images: 'The maximum number of virtual machines deployed at any time.', + total_failure_vm_slices: 'The total amount of CPU clock cycles lost due to failure.', + total_vms_submitted: 'The total number of virtual machines scheduled by the compute service.', + total_vms_queued: + 'The maximum number of virtual machines waiting to be scheduled by the compute service at any point.', + total_vms_finished: 'The total number of virtual machines that completed successfully.', + total_vms_failed: 'The total number of virtual machines that failed during execution.', +} diff --git a/opendc-web/opendc-web-ui/src/util/date-time.js b/opendc-web/opendc-web-ui/src/util/date-time.js index 66efdf5b..7e2f6623 100644 --- a/opendc-web/opendc-web-ui/src/util/date-time.js +++ b/opendc-web/opendc-web-ui/src/util/date-time.js @@ -7,19 +7,7 @@ * @returns {string} A human-friendly string version of that date and time. */ export function parseAndFormatDateTime(dateTimeString) { - return formatDateTime(parseDateTime(dateTimeString)) -} - -/** - * Parses date-time string representations and returns a parsed object. - * - * The format assumed is "YYYY-MM-DDTHH:MM:SS". - * - * @param dateTimeString A string expressing a date and a time, in the above mentioned format. - * @returns {object} A Date object with the parsed date and time information as content. - */ -export function parseDateTime(dateTimeString) { - return new Date(dateTimeString + '.000Z') + return formatDateTime(new Date(dateTimeString)) } /** diff --git a/opendc-web/opendc-web-ui/src/util/date-time.test.js b/opendc-web/opendc-web-ui/src/util/date-time.test.js index 3d95eba6..431e39f7 100644 --- a/opendc-web/opendc-web-ui/src/util/date-time.test.js +++ b/opendc-web/opendc-web-ui/src/util/date-time.test.js @@ -1,18 +1,4 @@ -import { convertSecondsToFormattedTime, parseDateTime } from './date-time' - -describe('date-time parsing', () => { - it('reads components properly', () => { - const dateString = '2017-09-27T20:55:01' - const parsedDate = parseDateTime(dateString) - - expect(parsedDate.getUTCFullYear()).toEqual(2017) - expect(parsedDate.getUTCMonth()).toEqual(8) - expect(parsedDate.getUTCDate()).toEqual(27) - expect(parsedDate.getUTCHours()).toEqual(20) - expect(parsedDate.getUTCMinutes()).toEqual(55) - expect(parsedDate.getUTCSeconds()).toEqual(1) - }) -}) +import { convertSecondsToFormattedTime } from './date-time' describe('tick formatting', () => { it("returns '0s' for numbers <= 0", () => { |
