diff options
Diffstat (limited to 'opendc-web/opendc-web-ui/src/components/topologies/map')
36 files changed, 1353 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/GrayContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/GrayContainer.js new file mode 100644 index 00000000..ccf637e5 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/GrayContainer.js @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import { useDispatch } from 'react-redux' +import { goDownOneInteractionLevel } from '../../../redux/actions/interaction-level' +import GrayLayer from './elements/GrayLayer' + +function GrayContainer() { + const dispatch = useDispatch() + const onClick = () => dispatch(goDownOneInteractionLevel()) + return <GrayLayer onClick={onClick} /> +} + +export default GrayContainer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/MapConstants.js b/opendc-web/opendc-web-ui/src/components/topologies/map/MapConstants.js new file mode 100644 index 00000000..4c3b2757 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/MapConstants.js @@ -0,0 +1,25 @@ +export const MAP_SIZE = 50 +export const TILE_SIZE_IN_PIXELS = 100 +export const TILE_SIZE_IN_METERS = 0.5 +export const MAP_SIZE_IN_PIXELS = MAP_SIZE * TILE_SIZE_IN_PIXELS + +export const OBJECT_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 5 +export const TILE_PLUS_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 3 +export const OBJECT_SIZE_IN_PIXELS = TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2 + +export const GRID_LINE_WIDTH_IN_PIXELS = 2 +export const WALL_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 16 +export const OBJECT_BORDER_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 16 +export const TILE_PLUS_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 10 + +export const RACK_FILL_ICON_WIDTH = OBJECT_SIZE_IN_PIXELS / 3 +export const RACK_FILL_ICON_OPACITY = 0.8 + +export const MAP_MOVE_PIXELS_PER_EVENT = 20 +export const MAP_SCALE_PER_EVENT = 1.1 +export const MAP_MIN_SCALE = 0.5 +export const MAP_MAX_SCALE = 1.5 + +export const MAX_NUM_UNITS_PER_MACHINE = 6 +export const DEFAULT_RACK_SLOT_CAPACITY = 42 +export const DEFAULT_RACK_POWER_CAPACITY = 10000 diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js b/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js new file mode 100644 index 00000000..5d19b3ad --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js @@ -0,0 +1,83 @@ +import React, { useRef, useState } from 'react' +import { HotKeys } from 'react-hotkeys' +import { Stage } from 'react-konva' +import { MAP_MAX_SCALE, MAP_MIN_SCALE, MAP_MOVE_PIXELS_PER_EVENT, MAP_SCALE_PER_EVENT } from './MapConstants' +import { Provider, useStore } from 'react-redux' +import useResizeObserver from 'use-resize-observer' +import { mapContainer } from './MapStage.module.scss' +import MapLayer from './layers/MapLayer' +import RoomHoverLayer from './layers/RoomHoverLayer' +import ObjectHoverLayer from './layers/ObjectHoverLayer' +import ScaleIndicator from './controls/ScaleIndicator' +import Toolbar from './controls/Toolbar' + +function MapStage() { + const store = useStore() + const { ref, width = 100, height = 100 } = useResizeObserver() + const stageRef = useRef(null) + const [[x, y], setPos] = useState([0, 0]) + const [scale, setScale] = useState(1) + + const clampScale = (target) => Math.min(Math.max(target, MAP_MIN_SCALE), MAP_MAX_SCALE) + const moveWithDelta = (deltaX, deltaY) => setPos(([x, y]) => [x + deltaX, y + deltaY]) + + const onZoom = (e) => { + e.evt.preventDefault() + + const stage = stageRef.current.getStage() + const oldScale = scale + + const pointer = stage.getPointerPosition() + const mousePointTo = { + x: (pointer.x - x) / oldScale, + y: (pointer.y - y) / oldScale, + } + + const newScale = clampScale(e.evt.deltaY > 0 ? oldScale * MAP_SCALE_PER_EVENT : oldScale / MAP_SCALE_PER_EVENT) + + setScale(newScale) + setPos([pointer.x - mousePointTo.x * newScale, pointer.y - mousePointTo.y * newScale]) + } + const onZoomButton = (zoomIn) => + setScale((scale) => clampScale(zoomIn ? scale * MAP_SCALE_PER_EVENT : scale / MAP_SCALE_PER_EVENT)) + const onDragEnd = (e) => setPos([e.target.x(), e.target.y()]) + const onExport = () => { + const download = document.createElement('a') + download.href = stageRef.current.getStage().toDataURL() + download.download = 'opendc-canvas-export-' + Date.now() + '.png' + download.click() + } + + const handlers = { + MOVE_LEFT: () => moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0), + MOVE_RIGHT: () => moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0), + MOVE_UP: () => moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT), + MOVE_DOWN: () => moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT), + } + + return ( + <HotKeys handlers={handlers} allowChanges={true} innerRef={ref} className={mapContainer}> + <Stage + ref={stageRef} + onWheel={onZoom} + onDragEnd={onDragEnd} + draggable + width={width} + height={height} + scale={{ x: scale, y: scale }} + x={x} + y={y} + > + <Provider store={store}> + <MapLayer /> + <RoomHoverLayer /> + <ObjectHoverLayer /> + </Provider> + </Stage> + <ScaleIndicator scale={scale} /> + <Toolbar onZoom={onZoomButton} onExport={onExport} /> + </HotKeys> + ) +} + +export default MapStage diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.module.scss b/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.module.scss new file mode 100644 index 00000000..d879b4c8 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.module.scss @@ -0,0 +1,31 @@ +/*! + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +.mapContainer { + background-color: var(--pf-global--Color--light-200); + position: relative; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RackContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RackContainer.js new file mode 100644 index 00000000..14449a91 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RackContainer.js @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import { useSelector } from 'react-redux' +import { Tile } from '../../../shapes' +import RackGroup from './groups/RackGroup' + +function RackContainer({ tile }) { + const interactionLevel = useSelector((state) => state.interactionLevel) + return <RackGroup interactionLevel={interactionLevel} tile={tile} /> +} + +RackContainer.propTypes = { + tile: Tile, +} + +export default RackContainer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js new file mode 100644 index 00000000..c35cbde7 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js @@ -0,0 +1,34 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { useSelector } from 'react-redux' +import RackFillBar from './elements/RackFillBar' + +function RackSpaceFillContainer({ tileId, ...props }) { + const fillFraction = useSelector((state) => { + let energyConsumptionTotal = 0 + const rack = state.objects.rack[state.objects.tile[tileId].rack] + const machineIds = rack.machines + machineIds.forEach((machineId) => { + if (machineId !== null) { + const machine = state.objects.machine[machineId] + machine.cpus.forEach((id) => (energyConsumptionTotal += state.objects.cpu[id].energyConsumptionW)) + machine.gpus.forEach((id) => (energyConsumptionTotal += state.objects.gpu[id].energyConsumptionW)) + machine.memories.forEach( + (id) => (energyConsumptionTotal += state.objects.memory[id].energyConsumptionW) + ) + machine.storages.forEach( + (id) => (energyConsumptionTotal += state.objects.storage[id].energyConsumptionW) + ) + } + }) + + return Math.min(1, energyConsumptionTotal / rack.powerCapacityW) + }) + return <RackFillBar {...props} type="energy" fillFraction={fillFraction} /> +} + +RackSpaceFillContainer.propTypes = { + tileId: PropTypes.string.isRequired, +} + +export default RackSpaceFillContainer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.js new file mode 100644 index 00000000..a6766f33 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.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 PropTypes from 'prop-types' +import { useSelector } from 'react-redux' +import RackFillBar from './elements/RackFillBar' + +function RackSpaceFillContainer({ tileId, ...props }) { + const rack = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack]) + return <RackFillBar {...props} type="space" fillFraction={rack.machines.length / rack.capacity} /> +} + +RackSpaceFillContainer.propTypes = { + tileId: PropTypes.string.isRequired, +} + +export default RackSpaceFillContainer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js new file mode 100644 index 00000000..93ba9c93 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import PropTypes from 'prop-types' +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { goFromBuildingToRoom } from '../../../redux/actions/interaction-level' +import RoomGroup from './groups/RoomGroup' + +function RoomContainer({ roomId, ...props }) { + const state = useSelector((state) => { + return { + interactionLevel: state.interactionLevel, + currentRoomInConstruction: state.construction.currentRoomInConstruction, + room: state.objects.room[roomId], + } + }) + const dispatch = useDispatch() + return <RoomGroup {...props} {...state} onClick={() => dispatch(goFromBuildingToRoom(roomId))} /> +} + +RoomContainer.propTypes = { + roomId: PropTypes.string, +} + +export default RoomContainer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js new file mode 100644 index 00000000..149e26a1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import PropTypes from 'prop-types' +import { useDispatch, useSelector } from 'react-redux' +import { goFromRoomToRack } from '../../../redux/actions/interaction-level' +import TileGroup from './groups/TileGroup' + +function TileContainer({ tileId, ...props }) { + const interactionLevel = useSelector((state) => state.interactionLevel) + const tile = useSelector((state) => state.objects.tile[tileId]) + + const dispatch = useDispatch() + const onClick = (tile) => { + if (tile.rack) { + dispatch(goFromRoomToRack(tile._id)) + } + } + return <TileGroup {...props} onClick={onClick} tile={tile} interactionLevel={interactionLevel} /> +} + +TileContainer.propTypes = { + tileId: PropTypes.string.isRequired, +} + +export default TileContainer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/TopologyContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/TopologyContainer.js new file mode 100644 index 00000000..eaebabd5 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/TopologyContainer.js @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import { useSelector } from 'react-redux' +import { useActiveTopology } from '../../../data/topology' +import TopologyGroup from './groups/TopologyGroup' + +function TopologyContainer() { + const topology = useActiveTopology() + const interactionLevel = useSelector((state) => state.interactionLevel) + + return <TopologyGroup topology={topology} interactionLevel={interactionLevel} /> +} + +export default TopologyContainer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.js new file mode 100644 index 00000000..77f553dd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.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 PropTypes from 'prop-types' +import { useSelector } from 'react-redux' +import WallGroup from './groups/WallGroup' + +function WallContainer({ roomId, ...props }) { + const tiles = useSelector((state) => state.objects.room[roomId].tiles.map((tileId) => state.objects.tile[tileId])) + return <WallGroup {...props} tiles={tiles} /> +} + +WallContainer.propTypes = { + roomId: PropTypes.string.isRequired, +} + +export default WallContainer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Collapse.js b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Collapse.js new file mode 100644 index 00000000..f54b7c84 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Collapse.js @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import PropTypes from 'prop-types' +import { ChevronLeftIcon } from '@patternfly/react-icons' +import { collapseContainer } from './Collapse.module.scss' +import { Button } from '@patternfly/react-core' + +function Collapse({ onClick }) { + return ( + <div className={collapseContainer}> + <Button variant="tertiary" onClick={onClick}> + <ChevronLeftIcon /> + </Button> + </div> + ) +} + +Collapse.propTypes = { + onClick: PropTypes.func, +} + +export default Collapse diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Collapse.module.scss b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Collapse.module.scss new file mode 100644 index 00000000..0c1fac94 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Collapse.module.scss @@ -0,0 +1,55 @@ +/*! + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +.collapseContainer { + position: absolute; + right: var(--pf-global--spacer--xs); + top: 0; + bottom: 10%; + margin: auto 0; + height: 50px; + + button:global(.pf-m-tertiary) { + height: 100%; + padding: 2px; + + margin-right: var(--pf-global--spacer--xs); + margin-top: var(--pf-global--spacer--xs); + background-color: var(--pf-global--BackgroundColor--100); + border: none; + border-radius: var(--pf-global--BorderRadius--sm); + box-shadow: var(--pf-global--BoxShadow--sm); + + &:not(:global(.pf-m-disabled)) { + background-color: var(--pf-global--BackgroundColor--100); + } + + &:after { + display: none; + } + + &:hover { + border: none; + box-shadow: var(--pf-global--BoxShadow--md); + } + } +} diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/controls/ScaleIndicator.js b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/ScaleIndicator.js new file mode 100644 index 00000000..58d2ccc9 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/ScaleIndicator.js @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { TILE_SIZE_IN_METERS, TILE_SIZE_IN_PIXELS } from '../MapConstants' +import { scaleIndicator } from './ScaleIndicator.module.scss' + +function ScaleIndicator({ scale }) { + return ( + <div className={scaleIndicator} style={{ width: TILE_SIZE_IN_PIXELS * scale }}> + {TILE_SIZE_IN_METERS}m + </div> + ) +} + +ScaleIndicator.propTypes = { + scale: PropTypes.number.isRequired, +} + +export default ScaleIndicator diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/controls/ScaleIndicator.module.scss b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/ScaleIndicator.module.scss new file mode 100644 index 00000000..f19e0ff2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/ScaleIndicator.module.scss @@ -0,0 +1,10 @@ +.scaleIndicator { + position: absolute; + right: 10px; + bottom: 10px; + z-index: 50; + + border: solid 2px #212529; + border-top: none; + border-left: none; +} diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Toolbar.js b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Toolbar.js new file mode 100644 index 00000000..469fd515 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Toolbar.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { control, toolBar } from './Toolbar.module.scss' +import { Button } from '@patternfly/react-core' +import { SearchPlusIcon, SearchMinusIcon } from '@patternfly/react-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCamera } from '@fortawesome/free-solid-svg-icons' + +function Toolbar({ onZoom, onExport }) { + return ( + <div className={toolBar}> + <Button variant="tertiary" title="Zoom in" onClick={() => onZoom(true)} className={control}> + <SearchPlusIcon /> + </Button> + <Button variant="tertiary" title="Zoom out" onClick={() => onZoom(false)} className={control}> + <SearchMinusIcon /> + </Button> + <Button + variant="tertiary" + title="Export Canvas to PNG Image" + onClick={() => onExport()} + className={control} + > + <FontAwesomeIcon icon={faCamera} /> + </Button> + </div> + ) +} + +Toolbar.propTypes = { + onZoom: PropTypes.func, + onExport: PropTypes.func, +} + +export default Toolbar diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Toolbar.module.scss b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Toolbar.module.scss new file mode 100644 index 00000000..0d505acc --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/controls/Toolbar.module.scss @@ -0,0 +1,29 @@ +.toolBar { + position: absolute; + bottom: var(--pf-global--spacer--md); + left: var(--pf-global--spacer--xl); +} + +.control { + &:global(.pf-m-tertiary) { + margin-right: var(--pf-global--spacer--xs); + margin-top: var(--pf-global--spacer--xs); + background-color: var(--pf-global--BackgroundColor--100); + border: none; + border-radius: var(--pf-global--BorderRadius--sm); + box-shadow: var(--pf-global--BoxShadow--sm); + + &:not(:global(.pf-m-disabled)) { + background-color: var(--pf-global--BackgroundColor--100); + } + + &:after { + display: none; + } + + &:hover { + border: none; + box-shadow: var(--pf-global--BoxShadow--md); + } + } +} diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/Backdrop.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/Backdrop.js new file mode 100644 index 00000000..93037b51 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/Backdrop.js @@ -0,0 +1,10 @@ +import React from 'react' +import { Rect } from 'react-konva' +import { BACKDROP_COLOR } from '../../../../util/colors' +import { MAP_SIZE_IN_PIXELS } from '../MapConstants' + +function Backdrop() { + return <Rect x={0} y={0} width={MAP_SIZE_IN_PIXELS} height={MAP_SIZE_IN_PIXELS} fill={BACKDROP_COLOR} /> +} + +export default Backdrop diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/GrayLayer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/GrayLayer.js new file mode 100644 index 00000000..08c687f6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/GrayLayer.js @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Rect } from 'react-konva' +import { GRAYED_OUT_AREA_COLOR } from '../../../../util/colors' +import { MAP_SIZE_IN_PIXELS } from '../MapConstants' + +function GrayLayer({ onClick }) { + return ( + <Rect + x={0} + y={0} + width={MAP_SIZE_IN_PIXELS} + height={MAP_SIZE_IN_PIXELS} + fill={GRAYED_OUT_AREA_COLOR} + onClick={onClick} + /> + ) +} + +GrayLayer.propTypes = { + onClick: PropTypes.func, +} + +export default GrayLayer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/HoverTile.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/HoverTile.js new file mode 100644 index 00000000..20c2c6d1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/HoverTile.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Rect } from 'react-konva' +import { ROOM_HOVER_INVALID_COLOR, ROOM_HOVER_VALID_COLOR } from '../../../../util/colors' +import { TILE_SIZE_IN_PIXELS } from '../MapConstants' + +function HoverTile({ x, y, isValid, scale = 1, onClick }) { + return ( + <Rect + x={x} + y={y} + scaleX={scale} + scaleY={scale} + width={TILE_SIZE_IN_PIXELS} + height={TILE_SIZE_IN_PIXELS} + fill={isValid ? ROOM_HOVER_VALID_COLOR : ROOM_HOVER_INVALID_COLOR} + onClick={onClick} + /> + ) +} + +HoverTile.propTypes = { + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + isValid: PropTypes.bool.isRequired, + scale: PropTypes.number, + onClick: PropTypes.func.isRequired, +} + +export default HoverTile diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js new file mode 100644 index 00000000..7d304b6b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/ImageComponent.js @@ -0,0 +1,36 @@ +import PropTypes from 'prop-types' +import React, { useEffect, useState } from 'react' +import { Image } from 'react-konva' + +const imageCaches = {} + +function ImageComponent({ src, x, y, width, height, opacity }) { + const [image, setImage] = useState(null) + + useEffect(() => { + if (imageCaches[src]) { + setImage(imageCaches[src]) + return + } + + const image = new window.Image() + image.src = src + image.onload = () => { + setImage(image) + imageCaches[src] = image + } + }, [src]) + + return <Image image={image} x={x} y={y} width={width} height={height} opacity={opacity} /> +} + +ImageComponent.propTypes = { + src: PropTypes.string.isRequired, + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + opacity: PropTypes.number.isRequired, +} + +export default ImageComponent diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/RackFillBar.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/RackFillBar.js new file mode 100644 index 00000000..aa284944 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/RackFillBar.js @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group, Rect } from 'react-konva' +import { + RACK_ENERGY_BAR_BACKGROUND_COLOR, + RACK_ENERGY_BAR_FILL_COLOR, + RACK_SPACE_BAR_BACKGROUND_COLOR, + RACK_SPACE_BAR_FILL_COLOR, +} from '../../../../util/colors' +import { + OBJECT_BORDER_WIDTH_IN_PIXELS, + OBJECT_MARGIN_IN_PIXELS, + RACK_FILL_ICON_OPACITY, + RACK_FILL_ICON_WIDTH, + TILE_SIZE_IN_PIXELS, +} from '../MapConstants' +import ImageComponent from './ImageComponent' + +function RackFillBar({ positionX, positionY, type, fillFraction }) { + const halfOfObjectBorderWidth = OBJECT_BORDER_WIDTH_IN_PIXELS / 2 + const x = + positionX * TILE_SIZE_IN_PIXELS + + OBJECT_MARGIN_IN_PIXELS + + (type === 'space' ? halfOfObjectBorderWidth : 0.5 * (TILE_SIZE_IN_PIXELS - 2 * OBJECT_MARGIN_IN_PIXELS)) + const startY = positionY * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS + halfOfObjectBorderWidth + const width = 0.5 * (TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2) - halfOfObjectBorderWidth + const fullHeight = TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2 - OBJECT_BORDER_WIDTH_IN_PIXELS + + const fractionHeight = fillFraction * fullHeight + const fractionY = + (positionY + 1) * TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS - halfOfObjectBorderWidth - fractionHeight + + return ( + <Group> + <Rect + x={x} + y={startY} + width={width} + height={fullHeight} + fill={type === 'space' ? RACK_SPACE_BAR_BACKGROUND_COLOR : RACK_ENERGY_BAR_BACKGROUND_COLOR} + /> + <Rect + x={x} + y={fractionY} + width={width} + height={fractionHeight} + fill={type === 'space' ? RACK_SPACE_BAR_FILL_COLOR : RACK_ENERGY_BAR_FILL_COLOR} + /> + <ImageComponent + src={'/img/topology/rack-' + type + '-icon.png'} + x={x + width * 0.5 - RACK_FILL_ICON_WIDTH * 0.5} + y={startY + fullHeight * 0.5 - RACK_FILL_ICON_WIDTH * 0.5} + width={RACK_FILL_ICON_WIDTH} + height={RACK_FILL_ICON_WIDTH} + opacity={RACK_FILL_ICON_OPACITY} + /> + </Group> + ) +} + +RackFillBar.propTypes = { + positionX: PropTypes.number.isRequired, + positionY: PropTypes.number.isRequired, + type: PropTypes.string.isRequired, + fillFraction: PropTypes.number.isRequired, +} + +export default RackFillBar diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/RoomTile.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/RoomTile.js new file mode 100644 index 00000000..e7329dc0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/RoomTile.js @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Rect } from 'react-konva' +import { Tile } from '../../../../shapes' +import { TILE_SIZE_IN_PIXELS } from '../MapConstants' + +function RoomTile({ tile, color }) { + return ( + <Rect + x={tile.positionX * TILE_SIZE_IN_PIXELS} + y={tile.positionY * TILE_SIZE_IN_PIXELS} + width={TILE_SIZE_IN_PIXELS} + height={TILE_SIZE_IN_PIXELS} + fill={color} + /> + ) +} + +RoomTile.propTypes = { + tile: Tile, + color: PropTypes.string, +} + +export default RoomTile diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/TileObject.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/TileObject.js new file mode 100644 index 00000000..3211f187 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/TileObject.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Rect } from 'react-konva' +import { OBJECT_BORDER_COLOR } from '../../../../util/colors' +import { OBJECT_BORDER_WIDTH_IN_PIXELS, OBJECT_MARGIN_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants' + +function TileObject({ positionX, positionY, color }) { + return ( + <Rect + x={positionX * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS} + y={positionY * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS} + width={TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2} + height={TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2} + fill={color} + stroke={OBJECT_BORDER_COLOR} + strokeWidth={OBJECT_BORDER_WIDTH_IN_PIXELS} + /> + ) +} + +TileObject.propTypes = { + positionX: PropTypes.number.isRequired, + positionY: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, +} + +export default TileObject diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/TilePlusIcon.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/TilePlusIcon.js new file mode 100644 index 00000000..186c2b3a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/TilePlusIcon.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group, Line } from 'react-konva' +import { TILE_PLUS_COLOR } from '../../../../util/colors' +import { TILE_PLUS_MARGIN_IN_PIXELS, TILE_PLUS_WIDTH_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants' + +function TilePlusIcon({ x, y, scale = 1 }) { + const linePoints = [ + [ + x + 0.5 * TILE_SIZE_IN_PIXELS * scale, + y + TILE_PLUS_MARGIN_IN_PIXELS * scale, + x + 0.5 * TILE_SIZE_IN_PIXELS * scale, + y + TILE_SIZE_IN_PIXELS * scale - TILE_PLUS_MARGIN_IN_PIXELS * scale, + ], + [ + x + TILE_PLUS_MARGIN_IN_PIXELS * scale, + y + 0.5 * TILE_SIZE_IN_PIXELS * scale, + x + TILE_SIZE_IN_PIXELS * scale - TILE_PLUS_MARGIN_IN_PIXELS * scale, + y + 0.5 * TILE_SIZE_IN_PIXELS * scale, + ], + ] + return ( + <Group> + {linePoints.map((points, index) => ( + <Line + key={index} + points={points} + lineCap="round" + stroke={TILE_PLUS_COLOR} + strokeWidth={TILE_PLUS_WIDTH_IN_PIXELS * scale} + listening={false} + /> + ))} + </Group> + ) +} + +TilePlusIcon.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + scale: PropTypes.number, +} + +export default TilePlusIcon diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/elements/WallSegment.js b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/WallSegment.js new file mode 100644 index 00000000..4f18813e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/elements/WallSegment.js @@ -0,0 +1,32 @@ +import React from 'react' +import { Line } from 'react-konva' +import { WallSegment as WallSegmentShape } from '../../../../shapes' +import { WALL_COLOR } from '../../../../util/colors' +import { TILE_SIZE_IN_PIXELS, WALL_WIDTH_IN_PIXELS } from '../MapConstants' + +function WallSegment({ wallSegment }) { + let points + if (wallSegment.isHorizontal) { + points = [ + wallSegment.startPosX * TILE_SIZE_IN_PIXELS, + wallSegment.startPosY * TILE_SIZE_IN_PIXELS, + (wallSegment.startPosX + wallSegment.length) * TILE_SIZE_IN_PIXELS, + wallSegment.startPosY * TILE_SIZE_IN_PIXELS, + ] + } else { + points = [ + wallSegment.startPosX * TILE_SIZE_IN_PIXELS, + wallSegment.startPosY * TILE_SIZE_IN_PIXELS, + wallSegment.startPosX * TILE_SIZE_IN_PIXELS, + (wallSegment.startPosY + wallSegment.length) * TILE_SIZE_IN_PIXELS, + ] + } + + return <Line points={points} lineCap="round" stroke={WALL_COLOR} strokeWidth={WALL_WIDTH_IN_PIXELS} /> +} + +WallSegment.propTypes = { + wallSegment: WallSegmentShape, +} + +export default WallSegment diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/GridGroup.js b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/GridGroup.js new file mode 100644 index 00000000..d66a18de --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/GridGroup.js @@ -0,0 +1,36 @@ +import React from 'react' +import { Group, Line } from 'react-konva' +import { GRID_COLOR } from '../../../../util/colors' +import { GRID_LINE_WIDTH_IN_PIXELS, MAP_SIZE, MAP_SIZE_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants' + +const MAP_COORDINATE_ENTRIES = Array.from(new Array(MAP_SIZE), (x, i) => i) +const HORIZONTAL_POINT_PAIRS = MAP_COORDINATE_ENTRIES.map((index) => [ + 0, + index * TILE_SIZE_IN_PIXELS, + MAP_SIZE_IN_PIXELS, + index * TILE_SIZE_IN_PIXELS, +]) +const VERTICAL_POINT_PAIRS = MAP_COORDINATE_ENTRIES.map((index) => [ + index * TILE_SIZE_IN_PIXELS, + 0, + index * TILE_SIZE_IN_PIXELS, + MAP_SIZE_IN_PIXELS, +]) + +function GridGroup() { + return ( + <Group> + {HORIZONTAL_POINT_PAIRS.concat(VERTICAL_POINT_PAIRS).map((points, index) => ( + <Line + key={index} + points={points} + stroke={GRID_COLOR} + strokeWidth={GRID_LINE_WIDTH_IN_PIXELS} + listening={false} + /> + ))} + </Group> + ) +} + +export default GridGroup diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js new file mode 100644 index 00000000..46030135 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RackGroup.js @@ -0,0 +1,25 @@ +import React from 'react' +import { Group } from 'react-konva' +import { Tile } from '../../../../shapes' +import { RACK_BACKGROUND_COLOR } from '../../../../util/colors' +import TileObject from '../elements/TileObject' +import RackSpaceFillContainer from '../RackSpaceFillContainer' +import RackEnergyFillContainer from '../RackEnergyFillContainer' + +function RackGroup({ tile }) { + return ( + <Group> + <TileObject positionX={tile.positionX} positionY={tile.positionY} color={RACK_BACKGROUND_COLOR} /> + <Group> + <RackSpaceFillContainer tileId={tile._id} positionX={tile.positionX} positionY={tile.positionY} /> + <RackEnergyFillContainer tileId={tile._id} positionX={tile.positionX} positionY={tile.positionY} /> + </Group> + </Group> + ) +} + +RackGroup.propTypes = { + tile: Tile, +} + +export default RackGroup diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js new file mode 100644 index 00000000..a42e7bb7 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/RoomGroup.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group } from 'react-konva' +import { InteractionLevel, Room } from '../../../../shapes' +import GrayContainer from '../GrayContainer' +import TileContainer from '../TileContainer' +import WallContainer from '../WallContainer' + +function RoomGroup({ room, interactionLevel, currentRoomInConstruction, onClick }) { + if (currentRoomInConstruction === room._id) { + return ( + <Group onClick={onClick}> + {room.tiles.map((tileId) => ( + <TileContainer key={tileId} tileId={tileId} newTile={true} /> + ))} + </Group> + ) + } + + return ( + <Group onClick={onClick}> + {(() => { + if ( + (interactionLevel.mode === 'RACK' || interactionLevel.mode === 'MACHINE') && + interactionLevel.roomId === room._id + ) { + return [ + room.tiles + .filter((tileId) => tileId !== interactionLevel.tileId) + .map((tileId) => <TileContainer key={tileId} tileId={tileId} />), + <GrayContainer key={-1} />, + room.tiles + .filter((tileId) => tileId === interactionLevel.tileId) + .map((tileId) => <TileContainer key={tileId} tileId={tileId} />), + ] + } else { + return room.tiles.map((tileId) => <TileContainer key={tileId} tileId={tileId} />) + } + })()} + <WallContainer roomId={room._id} /> + </Group> + ) +} + +RoomGroup.propTypes = { + room: Room, + interactionLevel: InteractionLevel, + currentRoomInConstruction: PropTypes.string, + onClick: PropTypes.func, +} + +export default RoomGroup diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/TileGroup.js b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/TileGroup.js new file mode 100644 index 00000000..f2084017 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/TileGroup.js @@ -0,0 +1,36 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group } from 'react-konva' +import { Tile } from '../../../../shapes' +import { ROOM_DEFAULT_COLOR, ROOM_IN_CONSTRUCTION_COLOR } from '../../../../util/colors' +import RoomTile from '../elements/RoomTile' +import RackContainer from '../RackContainer' + +function TileGroup({ tile, newTile, onClick }) { + let tileObject + if (tile.rack) { + tileObject = <RackContainer tile={tile} /> + } else { + tileObject = null + } + + let color = ROOM_DEFAULT_COLOR + if (newTile) { + color = ROOM_IN_CONSTRUCTION_COLOR + } + + return ( + <Group onClick={() => onClick(tile)}> + <RoomTile tile={tile} color={color} /> + {tileObject} + </Group> + ) +} + +TileGroup.propTypes = { + tile: Tile, + newTile: PropTypes.bool, + onClick: PropTypes.func, +} + +export default TileGroup diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/TopologyGroup.js b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/TopologyGroup.js new file mode 100644 index 00000000..011dcf34 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/TopologyGroup.js @@ -0,0 +1,44 @@ +import React from 'react' +import { Group } from 'react-konva' +import { InteractionLevel, Topology } from '../../../../shapes' +import RoomContainer from '../RoomContainer' +import GrayContainer from '../GrayContainer' + +function TopologyGroup({ topology, interactionLevel }) { + if (!topology) { + return <Group /> + } + + if (interactionLevel.mode === 'BUILDING') { + return ( + <Group> + {topology.rooms.map((roomId) => ( + <RoomContainer key={roomId} roomId={roomId} /> + ))} + </Group> + ) + } + + return ( + <Group> + {topology.rooms + .filter((roomId) => roomId !== interactionLevel.roomId) + .map((roomId) => ( + <RoomContainer key={roomId} roomId={roomId} /> + ))} + {interactionLevel.mode === 'ROOM' ? <GrayContainer /> : null} + {topology.rooms + .filter((roomId) => roomId === interactionLevel.roomId) + .map((roomId) => ( + <RoomContainer key={roomId} roomId={roomId} /> + ))} + </Group> + ) +} + +TopologyGroup.propTypes = { + topology: Topology, + interactionLevel: InteractionLevel, +} + +export default TopologyGroup diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/groups/WallGroup.js b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/WallGroup.js new file mode 100644 index 00000000..6cbd1cd0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/groups/WallGroup.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group } from 'react-konva' +import { Tile } from '../../../../shapes' +import { deriveWallLocations } from '../../../../util/tile-calculations' +import WallSegment from '../elements/WallSegment' + +function WallGroup({ tiles }) { + return ( + <Group> + {deriveWallLocations(tiles).map((wallSegment, index) => ( + <WallSegment key={index} wallSegment={wallSegment} /> + ))} + </Group> + ) +} + +WallGroup.propTypes = { + tiles: PropTypes.arrayOf(Tile).isRequired, +} + +export default WallGroup diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/HoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/HoverLayerComponent.js new file mode 100644 index 00000000..2b1060c0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/HoverLayerComponent.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types' +import React, { useMemo, useState } from 'react' +import { Layer } from 'react-konva/lib/ReactKonva' +import HoverTile from '../elements/HoverTile' +import { TILE_SIZE_IN_PIXELS } from '../MapConstants' +import { useEffectRef } from '../../../../util/effect-ref' + +function HoverLayerComponent({ isEnabled, isValid, onClick, children }) { + const [[mouseWorldX, mouseWorldY], setPos] = useState([0, 0]) + + const layerRef = useEffectRef((layer) => { + if (!layer) { + return + } + + const stage = layer.getStage() + + // Transform used to convert mouse coordinates to world coordinates + const transform = stage.getAbsoluteTransform().copy() + transform.invert() + + stage.on('mousemove.hover', () => { + const { x, y } = transform.point(stage.getPointerPosition()) + setPos([x, y]) + }) + return () => stage.off('mousemove.hover') + }) + + const gridX = Math.floor(mouseWorldX / TILE_SIZE_IN_PIXELS) + const gridY = Math.floor(mouseWorldY / TILE_SIZE_IN_PIXELS) + const valid = useMemo(() => isEnabled && isValid(gridX, gridY), [isEnabled, isValid, gridX, gridY]) + + if (!isEnabled) { + return <Layer /> + } + + const x = gridX * TILE_SIZE_IN_PIXELS + const y = gridY * TILE_SIZE_IN_PIXELS + + return ( + <Layer opacity={0.6} ref={layerRef}> + <HoverTile x={x} y={y} isValid={valid} onClick={() => (valid ? onClick(gridX, gridY) : undefined)} /> + {children ? React.cloneElement(children, { x, y, scale: 1 }) : undefined} + </Layer> + ) +} + +HoverLayerComponent.propTypes = { + isEnabled: PropTypes.bool.isRequired, + isValid: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, +} + +export default HoverLayerComponent diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/MapLayer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/MapLayer.js new file mode 100644 index 00000000..c902532b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/MapLayer.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 React from 'react' +import { Group, Layer } from 'react-konva' +import Backdrop from '../elements/Backdrop' +import TopologyContainer from '../TopologyContainer' +import GridGroup from '../groups/GridGroup' + +function MapLayer() { + return ( + <Layer> + <Group> + <Backdrop /> + <TopologyContainer /> + <GridGroup /> + </Group> + </Layer> + ) +} + +export default MapLayer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/ObjectHoverLayer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/ObjectHoverLayer.js new file mode 100644 index 00000000..47d9c992 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/ObjectHoverLayer.js @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { addRackToTile } from '../../../../redux/actions/topology/room' +import { findTileWithPosition } from '../../../../util/tile-calculations' +import HoverLayerComponent from './HoverLayerComponent' +import TilePlusIcon from '../elements/TilePlusIcon' + +function ObjectHoverLayer() { + const isEnabled = useSelector((state) => state.construction.inRackConstructionMode) + const isValid = useSelector((state) => (x, y) => { + if (state.interactionLevel.mode !== 'ROOM') { + return false + } + + const currentRoom = state.objects.room[state.interactionLevel.roomId] + const tiles = currentRoom.tiles.map((tileId) => state.objects.tile[tileId]) + const tile = findTileWithPosition(tiles, x, y) + + return !(tile === null || tile.rack) + }) + + const dispatch = useDispatch() + const onClick = (x, y) => dispatch(addRackToTile(x, y)) + return ( + <HoverLayerComponent onClick={onClick} isEnabled={isEnabled} isValid={isValid}> + <TilePlusIcon /> + </HoverLayerComponent> + ) +} + +export default ObjectHoverLayer diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js new file mode 100644 index 00000000..59f83b2b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.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 React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { toggleTileAtLocation } from '../../../../redux/actions/topology/building' +import { + deriveValidNextTilePositions, + findPositionInPositions, + findPositionInRooms, +} from '../../../../util/tile-calculations' +import HoverLayerComponent from './HoverLayerComponent' + +function RoomHoverLayer() { + const dispatch = useDispatch() + const onClick = (x, y) => dispatch(toggleTileAtLocation(x, y)) + const isEnabled = useSelector((state) => state.construction.currentRoomInConstruction !== '-1') + const isValid = useSelector((state) => (x, y) => { + const newRoom = { ...state.objects.room[state.construction.currentRoomInConstruction] } + const oldRooms = Object.keys(state.objects.room) + .map((id) => ({ ...state.objects.room[id] })) + .filter( + (room) => + state.objects.topology[state.currentTopologyId].rooms.indexOf(room._id) !== -1 && + room._id !== state.construction.currentRoomInConstruction + ) + + ;[...oldRooms, newRoom].forEach((room) => { + room.tiles = room.tiles.map((tileId) => state.objects.tile[tileId]) + }) + if (newRoom.tiles.length === 0) { + return findPositionInRooms(oldRooms, x, y) === -1 + } + + const validNextPositions = deriveValidNextTilePositions(oldRooms, newRoom.tiles) + return findPositionInPositions(validNextPositions, x, y) !== -1 + }) + + return <HoverLayerComponent onClick={onClick} isEnabled={isEnabled} isValid={isValid} /> +} + +export default RoomHoverLayer |
