summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar
diff options
context:
space:
mode:
authorvincent van beek <vincent@vlogic.nl>2026-03-26 14:02:54 +0100
committerGitHub <noreply@github.com>2026-03-26 13:02:54 +0000
commit0ffde21b0337c606e2d0ece5bd5434a930a87dcd (patch)
tree4fd071728a8da6dcf3e6fc9fe9dca5a492100d34 /opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar
parent8bb98c773bc982a0dab9cf9fb20d62f60a36a5d7 (diff)
Use Quarkus Quinoa for serving web UI (#391)
* refactor(web): Migrate to Quarkus 3 This commit updates the OpenDC web server to use Quarkus 3, which changes annotations to use the Jakarta namespace for annotations. * refactor(web): Configure runtime variables for web UI This commit updates the web UI to propagate runtime variables via the next-runtime-env package. Before, we would need to replace the variables in the generated sources by Next.js, next-runtime-env works by loading a JavaScript file when opening the OpenDC web UI which contains the configuration of the web app. * refactor(web): Migrate to Quarkus Quinoa This commit updates the OpenDC web server to make use of Quarkus Quinoa for serving the web UI. This allows us to deprecate the complex Quarkus extension for serving the web UI. * refactor(web): Move web UI into Quarkus web app This commit moves the web UI into the Quarkus web server module to ensure we follow Quarkus Quinoa's conventions. * refactor(web): Merge Quarkus extension into single module This commit merges the existing Quarkus extensions into a single module to prevent build complexity. * refactor(web): Migrate web proto to Java This commit migrates the web protocol to Java and removes the dependency on Jandex Gradle. * refactor(web): Migrate to Quarkus 3 This commit updates the OpenDC web server to use Quarkus 3, which changes annotations to use the Jakarta namespace for annotations. * enable DB schema migration on DEV server * webui is not needed anymore * remove MAINTAINERS is depricated * fix quarkus.quinoa properties * revert properties change, install npm in docker image to allow building the frontend * pin postgres version, this is a best practice. Fix some properties the old ones are depricated. Added properties for local testing * fix build error * :opendc-web:opendc-web-proto:spotlessApply * fix database schema --------- Co-authored-by: Fabian Mastenbroek <mail.fabianm@gmail.com>
Diffstat (limited to 'opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar')
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/NameComponent.js69
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.js83
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.module.css35
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/BuildingSidebar.js8
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionComponent.js46
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionContainer.js46
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/DeleteMachine.js59
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/MachineSidebar.js55
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddComponent.js42
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddContainer.js44
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListComponent.js113
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListContainer.js47
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitTabsComponent.js36
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitType.js25
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js41
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/DeleteRackContainer.js60
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineComponent.js40
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListComponent.js80
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListContainer.js56
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackNameContainer.js22
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.js58
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.module.css14
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/DeleteRoomContainer.js59
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/EditRoomContainer.js61
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js35
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js46
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomName.js44
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomSidebar.js43
28 files changed, 1367 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/NameComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/NameComponent.js
new file mode 100644
index 00000000..ececd07b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/NameComponent.js
@@ -0,0 +1,69 @@
+import PropTypes from 'prop-types'
+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,
+ onEdit: PropTypes.func,
+}
+
+export default NameComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.js
new file mode 100644
index 00000000..5aaa7834
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.js
@@ -0,0 +1,83 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { InteractionLevel } from '../../../shapes'
+import BuildingSidebar from './building/BuildingSidebar'
+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 { backButton } from './TopologySidebar.module.css'
+import RoomSidebar from './room/RoomSidebar'
+import RackSidebar from './rack/RackSidebar'
+import MachineSidebar from './machine/MachineSidebar'
+import { goDownOneInteractionLevel } from '../../../redux/actions/interaction-level'
+
+const name = {
+ BUILDING: 'Building',
+ ROOM: 'Room',
+ RACK: 'Rack',
+ MACHINE: 'Machine',
+}
+
+function TopologySidebar({ interactionLevel, onClose }) {
+ let sidebarContent
+
+ switch (interactionLevel.mode) {
+ case 'BUILDING':
+ sidebarContent = <BuildingSidebar />
+ break
+ case 'ROOM':
+ sidebarContent = <RoomSidebar roomId={interactionLevel.roomId} />
+ break
+ case 'RACK':
+ sidebarContent = <RackSidebar tileId={interactionLevel.tileId} />
+ break
+ case 'MACHINE':
+ sidebarContent = <MachineSidebar tileId={interactionLevel.tileId} position={interactionLevel.position} />
+ 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-server/src/main/webui/components/topologies/sidebar/TopologySidebar.module.css b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.module.css
new file mode 100644
index 00000000..3853c625
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/TopologySidebar.module.css
@@ -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.
+ */
+
+.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);
+}
+
+.backButton:hover,
+.backButton:global(.pf-c-button):focus {
+ --pf-c-button--after--BorderColor: var(--pf-global--BorderColor--100);
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/BuildingSidebar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/BuildingSidebar.js
new file mode 100644
index 00000000..5fcd46be
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/BuildingSidebar.js
@@ -0,0 +1,8 @@
+import React from 'react'
+import NewRoomConstructionContainer from './NewRoomConstructionContainer'
+
+function BuildingSidebar() {
+ return <NewRoomConstructionContainer />
+}
+
+export default BuildingSidebar
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionComponent.js
new file mode 100644
index 00000000..9fc85d0c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionComponent.js
@@ -0,0 +1,46 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+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'
+
+function NewRoomConstructionComponent({ onStart, onFinish, onCancel, currentRoomInConstruction }) {
+ if (currentRoomInConstruction === '-1') {
+ return (
+ <Button isBlock icon={<PlusIcon />} onClick={onStart}>
+ Construct a new room
+ </Button>
+ )
+ }
+ return (
+ <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>
+ )
+}
+
+NewRoomConstructionComponent.propTypes = {
+ onStart: PropTypes.func,
+ onFinish: PropTypes.func,
+ onCancel: PropTypes.func,
+ currentRoomInConstruction: PropTypes.string,
+}
+
+export default NewRoomConstructionComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/building/NewRoomConstructionContainer.js
new file mode 100644
index 00000000..c149b224
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/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'
+
+function NewRoomConstructionButton() {
+ const currentRoomInConstruction = useSelector((state) => state.construction.currentRoomInConstruction)
+ const dispatch = useDispatch()
+
+ return (
+ <NewRoomConstructionComponent
+ onStart={() => dispatch(startNewRoomConstruction())}
+ onFinish={() => dispatch(finishNewRoomConstruction())}
+ onCancel={() => dispatch(cancelNewRoomConstruction())}
+ currentRoomInConstruction={currentRoomInConstruction}
+ />
+ )
+}
+
+export default NewRoomConstructionButton
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/DeleteMachine.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/DeleteMachine.js
new file mode 100644
index 00000000..a4b9457b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/DeleteMachine.js
@@ -0,0 +1,59 @@
+/*
+ * 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, { useState } from 'react'
+import { useDispatch } from 'react-redux'
+import { Button } from '@patternfly/react-core'
+import { TrashIcon } from '@patternfly/react-icons'
+import ConfirmationModal from '../../../util/modals/ConfirmationModal'
+import { deleteMachine } from '../../../../redux/actions/topology/machine'
+
+function DeleteMachine({ machineId }) {
+ const dispatch = useDispatch()
+ const [isVisible, setVisible] = useState(false)
+ const callback = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteMachine(machineId))
+ }
+ 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}
+ />
+ </>
+ )
+}
+
+DeleteMachine.propTypes = {
+ machineId: PropTypes.string.isRequired,
+}
+
+export default DeleteMachine
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/MachineSidebar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/MachineSidebar.js
new file mode 100644
index 00000000..8a4c33dc
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/MachineSidebar.js
@@ -0,0 +1,55 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import UnitTabsComponent from './UnitTabsComponent'
+import DeleteMachine from './DeleteMachine'
+import {
+ TextContent,
+ TextList,
+ TextListItem,
+ TextListItemVariants,
+ TextListVariants,
+ Title,
+} from '@patternfly/react-core'
+import { useSelector } from 'react-redux'
+
+function MachineSidebar({ tileId, position }) {
+ const machine = useSelector(({ topology }) => {
+ const rack = topology.racks[topology.tiles[tileId].rack]
+
+ for (const machineId of rack.machines) {
+ const machine = topology.machines[machineId]
+ if (machine.position === position) {
+ return machine
+ }
+ }
+ })
+ const machineId = machine.id
+ return (
+ <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 machineId={machineId} />
+
+ <Title headingLevel="h2">Units</Title>
+ </TextContent>
+ <div className="pf-u-h-100">
+ <UnitTabsComponent machineId={machineId} />
+ </div>
+ </div>
+ )
+}
+
+MachineSidebar.propTypes = {
+ tileId: PropTypes.string.isRequired,
+ position: PropTypes.number.isRequired,
+}
+
+export default MachineSidebar
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddComponent.js
new file mode 100644
index 00000000..18cba23a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddComponent.js
@@ -0,0 +1,42 @@
+import PropTypes from 'prop-types'
+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 [isOpen, setOpen] = useState(false)
+ const [selected, setSelected] = useState(null)
+
+ return (
+ <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)} isDisabled={!selected}>
+ Add
+ </Button>
+ </InputGroup>
+ )
+}
+
+UnitAddComponent.propTypes = {
+ units: PropTypes.array.isRequired,
+ onAdd: PropTypes.func.isRequired,
+}
+
+export default UnitAddComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddContainer.js
new file mode 100644
index 00000000..a0054ef6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitAddContainer.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 UnitAddComponent from './UnitAddComponent'
+import { addUnit } from '../../../../redux/actions/topology/machine'
+import UnitType from './UnitType'
+
+function UnitAddContainer({ machineId, unitType }) {
+ const units = useSelector((state) => Object.values(state.topology[unitType]))
+ const dispatch = useDispatch()
+
+ const onAdd = (id) => dispatch(addUnit(machineId, unitType, id))
+
+ return <UnitAddComponent onAdd={onAdd} units={units} />
+}
+
+UnitAddContainer.propTypes = {
+ machineId: PropTypes.string.isRequired,
+ unitType: UnitType.isRequired,
+}
+
+export default UnitAddContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListComponent.js
new file mode 100644
index 00000000..75ab0ad7
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListComponent.js
@@ -0,0 +1,113 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+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'
+import { ProcessingUnit, StorageUnit } from '../../../../shapes'
+import UnitType from './UnitType'
+
+function UnitInfo({ unit, unitType }) {
+ if (unitType === 'cpus' || unitType === 'gpus') {
+ 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: UnitType.isRequired,
+ unit: PropTypes.oneOfType([ProcessingUnit, StorageUnit]).isRequired,
+}
+
+function 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: UnitType.isRequired,
+ units: PropTypes.arrayOf(PropTypes.oneOfType([ProcessingUnit, StorageUnit])).isRequired,
+ onDelete: PropTypes.func,
+}
+
+export default UnitListComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListContainer.js
new file mode 100644
index 00000000..bcd4bdcc
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitListContainer.js
@@ -0,0 +1,47 @@
+/*
+ * 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'
+import UnitType from './UnitType'
+
+function UnitListContainer({ machineId, unitType }) {
+ const dispatch = useDispatch()
+ const units = useSelector((state) => {
+ const machine = state.topology.machines[machineId]
+ return machine[unitType].map((id) => state.topology[unitType][id])
+ })
+
+ const onDelete = (unit) => dispatch(deleteUnit(machineId, unitType, unit.id))
+
+ return <UnitListComponent units={units} unitType={unitType} onDelete={onDelete} />
+}
+
+UnitListContainer.propTypes = {
+ machineId: PropTypes.string.isRequired,
+ unitType: UnitType.isRequired,
+}
+
+export default UnitListContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitTabsComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitTabsComponent.js
new file mode 100644
index 00000000..4032d607
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitTabsComponent.js
@@ -0,0 +1,36 @@
+import PropTypes from 'prop-types'
+import React, { useState } from 'react'
+import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'
+import UnitAddContainer from './UnitAddContainer'
+import UnitListContainer from './UnitListContainer'
+
+function UnitTabsComponent({ machineId }) {
+ const [activeTab, setActiveTab] = useState('cpuModel-units')
+
+ return (
+ <Tabs activeKey={activeTab} onSelect={(_, tab) => setActiveTab(tab)}>
+ <Tab eventKey="cpuModel-units" title={<TabTitleText>CPU</TabTitleText>}>
+ <UnitAddContainer machineId={machineId} unitType="cpus" />
+ <UnitListContainer machineId={machineId} unitType="cpus" />
+ </Tab>
+ <Tab eventKey="gpu-units" title={<TabTitleText>GPU</TabTitleText>}>
+ <UnitAddContainer machineId={machineId} unitType="gpus" />
+ <UnitListContainer machineId={machineId} unitType="gpus" />
+ </Tab>
+ <Tab eventKey="memory-units" title={<TabTitleText>Memory</TabTitleText>}>
+ <UnitAddContainer machineId={machineId} unitType="memories" />
+ <UnitListContainer machineId={machineId} unitType="memories" />
+ </Tab>
+ <Tab eventKey="storage-units" title={<TabTitleText>Storage</TabTitleText>}>
+ <UnitAddContainer machineId={machineId} unitType="storages" />
+ <UnitListContainer machineId={machineId} unitType="storages" />
+ </Tab>
+ </Tabs>
+ )
+}
+
+UnitTabsComponent.propTypes = {
+ machineId: PropTypes.string.isRequired,
+}
+
+export default UnitTabsComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitType.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitType.js
new file mode 100644
index 00000000..b6d7bf8b
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/machine/UnitType.js
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2022 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'
+
+export default PropTypes.oneOf(['cpus', 'gpus', 'memories', 'storages'])
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js
new file mode 100644
index 00000000..6a0c3ff3
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.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 React from 'react'
+import { Button } from '@patternfly/react-core'
+import { SaveIcon } from '@patternfly/react-icons'
+
+function AddPrefab() {
+ const onClick = () => {} // TODO
+ return (
+ <Button variant="primary" icon={<SaveIcon />} isBlock onClick={onClick} className="pf-u-mb-sm">
+ Save this rack to a prefab
+ </Button>
+ )
+}
+
+AddPrefab.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default AddPrefab
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/DeleteRackContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/DeleteRackContainer.js
new file mode 100644
index 00000000..0583a7a4
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/DeleteRackContainer.js
@@ -0,0 +1,60 @@
+/*
+ * 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, { useState } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon'
+import { Button } from '@patternfly/react-core'
+import ConfirmationModal from '../../../util/modals/ConfirmationModal'
+import { deleteRack } from '../../../../redux/actions/topology/rack'
+
+function DeleteRackContainer({ tileId }) {
+ const dispatch = useDispatch()
+ const [isVisible, setVisible] = useState(false)
+ const rackId = useSelector((state) => state.topology.tiles[tileId].rack)
+ const callback = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteRack(tileId, rackId))
+ }
+ 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}
+ />
+ </>
+ )
+}
+
+DeleteRackContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default DeleteRackContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineComponent.js
new file mode 100644
index 00000000..b0a96a9f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineComponent.js
@@ -0,0 +1,40 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Flex, Label } from '@patternfly/react-core'
+import { Machine } from '../../../../shapes'
+
+const UnitIcon = ({ id, type }) => (
+ // eslint-disable-next-line @next/next/no-img-element
+ <img src={'/img/topology/' + id + '-icon.png'} alt={'Machine contains ' + type + ' units'} height={24} width={24} />
+)
+
+UnitIcon.propTypes = {
+ id: PropTypes.string,
+ type: PropTypes.string,
+}
+
+function MachineComponent({ machine, onClick }) {
+ const hasNoUnits =
+ machine.cpus.length + machine.gpus.length + machine.memories.length + machine.storages.length === 0
+
+ return (
+ <Flex onClick={() => onClick()}>
+ {machine.cpus.length > 0 ? <UnitIcon id="cpuModel" 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.isRequired,
+ onClick: PropTypes.func,
+}
+
+export default MachineComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListComponent.js
new file mode 100644
index 00000000..02c97730
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListComponent.js
@@ -0,0 +1,80 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import MachineComponent from './MachineComponent'
+import {
+ Badge,
+ Button,
+ DataList,
+ DataListAction,
+ DataListCell,
+ DataListItem,
+ DataListItemCells,
+ DataListItemRow,
+} from '@patternfly/react-core'
+import { AngleRightIcon, PlusIcon } from '@patternfly/react-icons'
+import { Machine } from '../../../../shapes'
+
+function MachineListComponent({ machines = [], onSelect, onAdd }) {
+ return (
+ <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>{index + 1}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>{index + 1}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>
+ )
+ )
+ .reverse()}
+ </DataList>
+ )
+}
+
+MachineListComponent.propTypes = {
+ machines: PropTypes.arrayOf(Machine),
+ onSelect: PropTypes.func.isRequired,
+ onAdd: PropTypes.func.isRequired,
+}
+
+export default MachineListComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/MachineListContainer.js
new file mode 100644
index 00000000..e1914730
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/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.topology.racks[state.topology.tiles[tileId].rack])
+ const machines = useSelector((state) => rack.machines.map((id) => state.topology.machines[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(rack.id, index))}
+ onSelect={(index) => dispatch(goFromRackToMachine(index))}
+ />
+ )
+}
+
+MachineListContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default MachineListContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackNameContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackNameContainer.js
new file mode 100644
index 00000000..c3422318
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/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 { name: rackName, id } = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack])
+ const dispatch = useDispatch()
+ const callback = (name) => {
+ if (name) {
+ dispatch(editRackName(id, name))
+ }
+ }
+ return <NameComponent name={rackName} onEdit={callback} />
+}
+
+RackNameContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default RackNameContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.js
new file mode 100644
index 00000000..cb7d3b68
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.js
@@ -0,0 +1,58 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { machineListContainer, sidebarContainer } from './RackSidebar.module.css'
+import RackNameContainer from './RackNameContainer'
+import AddPrefab from './AddPrefab'
+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 RackSidebar({ tileId }) {
+ const rack = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack])
+
+ return (
+ <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>
+ <AddPrefab tileId={tileId} />
+ <DeleteRackContainer tileId={tileId} />
+
+ <Title headingLevel="h2">Slots</Title>
+ </TextContent>
+ <div className={machineListContainer}>
+ <MachineListContainer tileId={tileId} />
+ </div>
+ </div>
+ )
+}
+
+RackSidebar.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default RackSidebar
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.module.css b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.module.css
new file mode 100644
index 00000000..f4c8829f
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/RackSidebar.module.css
@@ -0,0 +1,14 @@
+.sidebarContainer {
+ display: flex;
+ flex-direction: column;
+
+ height: 100%;
+}
+
+.machineListContainer {
+ overflow-y: auto;
+
+ flex: 1 0 300px;
+
+ margin-top: 10px;
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/DeleteRoomContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/DeleteRoomContainer.js
new file mode 100644
index 00000000..29b8f78a
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/DeleteRoomContainer.js
@@ -0,0 +1,59 @@
+/*
+ * 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, { useState } from 'react'
+import { useDispatch } from 'react-redux'
+import ConfirmationModal from '../../../util/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'
+
+function DeleteRoomContainer({ roomId }) {
+ const dispatch = useDispatch()
+ const [isVisible, setVisible] = useState(false)
+ const callback = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteRoom(roomId))
+ }
+ 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}
+ />
+ </>
+ )
+}
+
+DeleteRoomContainer.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default DeleteRoomContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/EditRoomContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/EditRoomContainer.js
new file mode 100644
index 00000000..7a278cd6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/EditRoomContainer.js
@@ -0,0 +1,61 @@
+/*
+ * 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 { 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'
+
+function EditRoomContainer({ roomId }) {
+ const isEditing = useSelector((state) => state.construction.currentRoomInConstruction !== '-1')
+ const isInRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode)
+
+ const dispatch = useDispatch()
+ const onEdit = () => dispatch(startRoomEdit(roomId))
+ 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>
+ )
+}
+
+EditRoomContainer.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default EditRoomContainer
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js
new file mode 100644
index 00000000..a384d5d5
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js
@@ -0,0 +1,35 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Button } from '@patternfly/react-core'
+import { PlusIcon, TimesIcon } from '@patternfly/react-icons'
+
+const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, isEditingRoom }) => {
+ if (inRackConstructionMode) {
+ return (
+ <Button isBlock={true} icon={<TimesIcon />} onClick={onStop} className="pf-u-mb-sm">
+ Stop rack construction
+ </Button>
+ )
+ }
+
+ return (
+ <Button
+ icon={<PlusIcon />}
+ isBlock
+ isDisabled={isEditingRoom}
+ onClick={() => (isEditingRoom ? undefined : onStart())}
+ className="pf-u-mb-sm"
+ >
+ Start rack construction
+ </Button>
+ )
+}
+
+RackConstructionComponent.propTypes = {
+ onStart: PropTypes.func,
+ onStop: PropTypes.func,
+ inRackConstructionMode: PropTypes.bool,
+ isEditingRoom: PropTypes.bool,
+}
+
+export default RackConstructionComponent
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js
new file mode 100644
index 00000000..e04287a5
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/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'
+
+function 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-server/src/main/webui/components/topologies/sidebar/room/RoomName.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomName.js
new file mode 100644
index 00000000..72d45bea
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/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 '../NameComponent'
+import { editRoomName } from '../../../../redux/actions/topology/room'
+
+function RoomName({ roomId }) {
+ const { name: roomName, id } = useSelector((state) => state.topology.rooms[roomId])
+ const dispatch = useDispatch()
+ const callback = (name) => {
+ if (name) {
+ dispatch(editRoomName(id, name))
+ }
+ }
+ return <NameComponent name={roomName} onEdit={callback} />
+}
+
+RoomName.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default RoomName
diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomSidebar.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomSidebar.js
new file mode 100644
index 00000000..6ad489e0
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RoomSidebar.js
@@ -0,0 +1,43 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+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 RoomSidebar = ({ roomId }) => {
+ return (
+ <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 roomId={roomId} />
+ <DeleteRoomContainer roomId={roomId} />
+ </TextContent>
+ )
+}
+
+RoomSidebar.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default RoomSidebar