diff options
Diffstat (limited to 'opendc-web/opendc-web-ui/src/components/app/sidebars')
45 files changed, 1222 insertions, 745 deletions
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 |
