diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-04-25 16:01:14 +0200 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-04-25 16:01:14 +0200 |
| commit | cd0b45627f0d8da8c8dc4edde223f3c36e9bcfbf (patch) | |
| tree | 6ae1681630a0e270c23804e6dbb3bd414ebe5d6e /opendc-web/opendc-web-ui/src/components/app | |
| parent | 128a1db017545597a5c035b7960eb3fd36b5f987 (diff) | |
build: Migrate to flat project structure
This change updates the project structure to become flattened.
Previously, the simulator, frontend and API each lived into their own
directory.
With this change, all modules of the project live in the top-level
directory of the repository. This should improve discoverability of
modules of the project.
Diffstat (limited to 'opendc-web/opendc-web-ui/src/components/app')
63 files changed, 1778 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js b/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js new file mode 100644 index 00000000..7efea9b0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js @@ -0,0 +1,11 @@ +import React from 'react' +import FontAwesome from 'react-fontawesome' + +const LoadingScreen = () => ( + <div className="display-4"> + <FontAwesome name="refresh" className="mr-4" spin /> + Loading your project... + </div> +) + +export default LoadingScreen diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js new file mode 100644 index 00000000..d6ea1f84 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js @@ -0,0 +1,28 @@ +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 / 8 +export const OBJECT_BORDER_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 12 +export const TILE_PLUS_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 10 + +export const SIDEBAR_WIDTH = 350 +export const VIEWPORT_PADDING = 50 + +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/app/map/MapStageComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js new file mode 100644 index 00000000..2cd0ed6e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js @@ -0,0 +1,103 @@ +import React from 'react' +import { Stage } from 'react-konva' +import { Shortcuts } from 'react-shortcuts' +import MapLayer from '../../../containers/app/map/layers/MapLayer' +import ObjectHoverLayer from '../../../containers/app/map/layers/ObjectHoverLayer' +import RoomHoverLayer from '../../../containers/app/map/layers/RoomHoverLayer' +import { NAVBAR_HEIGHT } from '../../navigation/Navbar' +import { MAP_MOVE_PIXELS_PER_EVENT } from './MapConstants' +import { Provider } from 'react-redux' +import { store } from '../../../store/configure-store' + +class MapStageComponent extends React.Component { + state = { + mouseX: 0, + mouseY: 0, + } + + constructor(props) { + super(props) + + this.updateDimensions = this.updateDimensions.bind(this) + this.updateScale = this.updateScale.bind(this) + } + + componentDidMount() { + this.updateDimensions() + + window.addEventListener('resize', this.updateDimensions) + window.addEventListener('wheel', this.updateScale) + + window['exportCanvasToImage'] = () => { + const download = document.createElement('a') + download.href = this.stage.getStage().toDataURL() + download.download = 'opendc-canvas-export-' + Date.now() + '.png' + download.click() + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.updateDimensions) + window.removeEventListener('wheel', this.updateScale) + } + + updateDimensions() { + this.props.setMapDimensions(window.innerWidth, window.innerHeight - NAVBAR_HEIGHT) + } + + updateScale(e) { + e.preventDefault() + this.props.zoomInOnPosition(e.deltaY < 0, this.state.mouseX, this.state.mouseY) + } + + updateMousePosition() { + const mousePos = this.stage.getStage().getPointerPosition() + this.setState({ mouseX: mousePos.x, mouseY: mousePos.y }) + } + + handleShortcuts(action) { + switch (action) { + case 'MOVE_LEFT': + this.moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0) + break + case 'MOVE_RIGHT': + this.moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0) + break + case 'MOVE_UP': + this.moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT) + break + case 'MOVE_DOWN': + this.moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT) + break + default: + break + } + } + + moveWithDelta(deltaX, deltaY) { + this.props.setMapPositionWithBoundsCheck(this.props.mapPosition.x + deltaX, this.props.mapPosition.y + deltaY) + } + + render() { + return ( + <Shortcuts name="MAP" handler={this.handleShortcuts.bind(this)} targetNodeSelector="body"> + <Stage + ref={(stage) => { + this.stage = stage + }} + width={this.props.mapDimensions.width} + height={this.props.mapDimensions.height} + onMouseMove={this.updateMousePosition.bind(this)} + > + <Provider store={store}> + <MapLayer /> + <RoomHoverLayer mouseX={this.state.mouseX} mouseY={this.state.mouseY} /> + <ObjectHoverLayer mouseX={this.state.mouseX} mouseY={this.state.mouseY} /> + </Provider> + </Stage> + </Shortcuts> + ) + } +} + +export default MapStageComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js new file mode 100644 index 00000000..8487f47b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js @@ -0,0 +1,13 @@ +import React from 'react' + +const ExportCanvasComponent = () => ( + <button + className="btn btn-success btn-circle btn-sm" + title="Export Canvas to PNG Image" + onClick={() => window['exportCanvasToImage']()} + > + <span className="fa fa-camera" /> + </button> +) + +export default ExportCanvasComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js new file mode 100644 index 00000000..7cbb45c0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js @@ -0,0 +1,11 @@ +import React from 'react' +import { TILE_SIZE_IN_METERS, TILE_SIZE_IN_PIXELS } from '../MapConstants' +import './ScaleIndicatorComponent.sass' + +const ScaleIndicatorComponent = ({ scale }) => ( + <div className="scale-indicator" style={{ width: TILE_SIZE_IN_PIXELS * scale }}> + {TILE_SIZE_IN_METERS}m + </div> +) + +export default ScaleIndicatorComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.sass b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.sass new file mode 100644 index 00000000..03a72c99 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.sass @@ -0,0 +1,9 @@ +.scale-indicator + 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/app/map/controls/ToolPanelComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js new file mode 100644 index 00000000..f372734d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js @@ -0,0 +1,13 @@ +import React from 'react' +import ZoomControlContainer from '../../../../containers/app/map/controls/ZoomControlContainer' +import ExportCanvasComponent from './ExportCanvasComponent' +import './ToolPanelComponent.sass' + +const ToolPanelComponent = () => ( + <div className="tool-panel"> + <ZoomControlContainer /> + <ExportCanvasComponent /> + </div> +) + +export default ToolPanelComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.sass b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.sass new file mode 100644 index 00000000..8b27d24a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.sass @@ -0,0 +1,5 @@ +.tool-panel + position: absolute + left: 10px + bottom: 10px + z-index: 50 diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js new file mode 100644 index 00000000..65944bea --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js @@ -0,0 +1,24 @@ +import React from 'react' + +const ZoomControlComponent = ({ zoomInOnCenter }) => { + return ( + <span> + <button + className="btn btn-default btn-circle btn-sm mr-1" + title="Zoom in" + onClick={() => zoomInOnCenter(true)} + > + <span className="fa fa-plus" /> + </button> + <button + className="btn btn-default btn-circle btn-sm mr-1" + title="Zoom out" + onClick={() => zoomInOnCenter(false)} + > + <span className="fa fa-minus" /> + </button> + </span> + ) +} + +export default ZoomControlComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/Backdrop.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/Backdrop.js new file mode 100644 index 00000000..8ccfe584 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/Backdrop.js @@ -0,0 +1,8 @@ +import React from 'react' +import { Rect } from 'react-konva' +import { BACKDROP_COLOR } from '../../../../util/colors' +import { MAP_SIZE_IN_PIXELS } from '../MapConstants' + +const Backdrop = () => <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/app/map/elements/GrayLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/GrayLayer.js new file mode 100644 index 00000000..c54a34ad --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/GrayLayer.js @@ -0,0 +1,17 @@ +import React from 'react' +import { Rect } from 'react-konva' +import { GRAYED_OUT_AREA_COLOR } from '../../../../util/colors' +import { MAP_SIZE_IN_PIXELS } from '../MapConstants' + +const GrayLayer = ({ onClick }) => ( + <Rect + x={0} + y={0} + width={MAP_SIZE_IN_PIXELS} + height={MAP_SIZE_IN_PIXELS} + fill={GRAYED_OUT_AREA_COLOR} + onClick={onClick} + /> +) + +export default GrayLayer diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js new file mode 100644 index 00000000..912229c4 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js @@ -0,0 +1,27 @@ +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' + +const HoverTile = ({ pixelX, pixelY, isValid, scale, onClick }) => ( + <Rect + x={pixelX} + y={pixelY} + 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 = { + pixelX: PropTypes.number.isRequired, + pixelY: PropTypes.number.isRequired, + isValid: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, +} + +export default HoverTile diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/ImageComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/ImageComponent.js new file mode 100644 index 00000000..2b5c569f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/ImageComponent.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Image } from 'react-konva' + +class ImageComponent extends React.Component { + static imageCaches = {} + static 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, + } + + state = { + image: null, + } + + componentDidMount() { + if (ImageComponent.imageCaches[this.props.src]) { + this.setState({ image: ImageComponent.imageCaches[this.props.src] }) + return + } + + const image = new window.Image() + image.src = this.props.src + image.onload = () => { + this.setState({ image }) + ImageComponent.imageCaches[this.props.src] = image + } + } + + render() { + return ( + <Image + image={this.state.image} + x={this.props.x} + y={this.props.y} + width={this.props.width} + height={this.props.height} + opacity={this.props.opacity} + /> + ) + } +} + +export default ImageComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js new file mode 100644 index 00000000..8c573a6f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/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' + +const 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/app/map/elements/RoomTile.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/RoomTile.js new file mode 100644 index 00000000..43bf918e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/RoomTile.js @@ -0,0 +1,20 @@ +import React from 'react' +import { Rect } from 'react-konva' +import Shapes from '../../../../shapes/index' +import { TILE_SIZE_IN_PIXELS } from '../MapConstants' + +const RoomTile = ({ tile, color }) => ( + <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: Shapes.Tile, +} + +export default RoomTile diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/TileObject.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/TileObject.js new file mode 100644 index 00000000..9e87cc82 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/TileObject.js @@ -0,0 +1,25 @@ +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' + +const TileObject = ({ positionX, positionY, color }) => ( + <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/app/map/elements/TilePlusIcon.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js new file mode 100644 index 00000000..be3a00a8 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/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' + +const TilePlusIcon = ({ pixelX, pixelY, mapScale }) => { + const linePoints = [ + [ + pixelX + 0.5 * TILE_SIZE_IN_PIXELS * mapScale, + pixelY + TILE_PLUS_MARGIN_IN_PIXELS * mapScale, + pixelX + 0.5 * TILE_SIZE_IN_PIXELS * mapScale, + pixelY + TILE_SIZE_IN_PIXELS * mapScale - TILE_PLUS_MARGIN_IN_PIXELS * mapScale, + ], + [ + pixelX + TILE_PLUS_MARGIN_IN_PIXELS * mapScale, + pixelY + 0.5 * TILE_SIZE_IN_PIXELS * mapScale, + pixelX + TILE_SIZE_IN_PIXELS * mapScale - TILE_PLUS_MARGIN_IN_PIXELS * mapScale, + pixelY + 0.5 * TILE_SIZE_IN_PIXELS * mapScale, + ], + ] + return ( + <Group> + {linePoints.map((points, index) => ( + <Line + key={index} + points={points} + lineCap="round" + stroke={TILE_PLUS_COLOR} + strokeWidth={TILE_PLUS_WIDTH_IN_PIXELS * mapScale} + listening={false} + /> + ))} + </Group> + ) +} + +TilePlusIcon.propTypes = { + pixelX: PropTypes.number, + pixelY: PropTypes.number, + mapScale: PropTypes.number, +} + +export default TilePlusIcon diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/WallSegment.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/WallSegment.js new file mode 100644 index 00000000..8aa2aebf --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/WallSegment.js @@ -0,0 +1,32 @@ +import React from 'react' +import { Line } from 'react-konva' +import Shapes from '../../../../shapes/index' +import { WALL_COLOR } from '../../../../util/colors' +import { TILE_SIZE_IN_PIXELS, WALL_WIDTH_IN_PIXELS } from '../MapConstants' + +const 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: Shapes.WallSegment, +} + +export default WallSegment diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/GridGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/GridGroup.js new file mode 100644 index 00000000..ebc00244 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/GridGroup.js @@ -0,0 +1,34 @@ +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, +]) + +const GridGroup = () => ( + <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/app/map/groups/RackGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js new file mode 100644 index 00000000..eb6dc24a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js @@ -0,0 +1,25 @@ +import React from 'react' +import { Group } from 'react-konva' +import RackEnergyFillContainer from '../../../../containers/app/map/RackEnergyFillContainer' +import RackSpaceFillContainer from '../../../../containers/app/map/RackSpaceFillContainer' +import Shapes from '../../../../shapes/index' +import { RACK_BACKGROUND_COLOR } from '../../../../util/colors' +import TileObject from '../elements/TileObject' + +const 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: Shapes.Tile, +} + +export default RackGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js new file mode 100644 index 00000000..1fd54687 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js @@ -0,0 +1,48 @@ +import React from 'react' +import { Group } from 'react-konva' +import GrayContainer from '../../../../containers/app/map/GrayContainer' +import TileContainer from '../../../../containers/app/map/TileContainer' +import WallContainer from '../../../../containers/app/map/WallContainer' +import Shapes from '../../../../shapes/index' + +const RoomGroup = ({ room, interactionLevel, currentRoomInConstruction, onClick }) => { + if (currentRoomInConstruction === room._id) { + return ( + <Group onClick={onClick}> + {room.tileIds.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.tileIds + .filter((tileId) => tileId !== interactionLevel.tileId) + .map((tileId) => <TileContainer key={tileId} tileId={tileId} />), + <GrayContainer key={-1} />, + room.tileIds + .filter((tileId) => tileId === interactionLevel.tileId) + .map((tileId) => <TileContainer key={tileId} tileId={tileId} />), + ] + } else { + return room.tileIds.map((tileId) => <TileContainer key={tileId} tileId={tileId} />) + } + })()} + <WallContainer roomId={room._id} /> + </Group> + ) +} + +RoomGroup.propTypes = { + room: Shapes.Room, +} + +export default RoomGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js new file mode 100644 index 00000000..1e106823 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group } from 'react-konva' +import RackContainer from '../../../../containers/app/map/RackContainer' +import Shapes from '../../../../shapes/index' +import { ROOM_DEFAULT_COLOR, ROOM_IN_CONSTRUCTION_COLOR } from '../../../../util/colors' +import RoomTile from '../elements/RoomTile' + +const TileGroup = ({ tile, newTile, roomLoad, onClick }) => { + let tileObject + if (tile.rackId) { + 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: Shapes.Tile, + newTile: PropTypes.bool, +} + +export default TileGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js new file mode 100644 index 00000000..6096fc8b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js @@ -0,0 +1,44 @@ +import React from 'react' +import { Group } from 'react-konva' +import GrayContainer from '../../../../containers/app/map/GrayContainer' +import RoomContainer from '../../../../containers/app/map/RoomContainer' +import Shapes from '../../../../shapes/index' + +const TopologyGroup = ({ topology, interactionLevel }) => { + if (!topology) { + return <Group /> + } + + if (interactionLevel.mode === 'BUILDING') { + return ( + <Group> + {topology.roomIds.map((roomId) => ( + <RoomContainer key={roomId} roomId={roomId} /> + ))} + </Group> + ) + } + + return ( + <Group> + {topology.roomIds + .filter((roomId) => roomId !== interactionLevel.roomId) + .map((roomId) => ( + <RoomContainer key={roomId} roomId={roomId} /> + ))} + {interactionLevel.mode === 'ROOM' ? <GrayContainer /> : null} + {topology.roomIds + .filter((roomId) => roomId === interactionLevel.roomId) + .map((roomId) => ( + <RoomContainer key={roomId} roomId={roomId} /> + ))} + </Group> + ) +} + +TopologyGroup.propTypes = { + topology: Shapes.Topology, + interactionLevel: Shapes.InteractionLevel, +} + +export default TopologyGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/WallGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/WallGroup.js new file mode 100644 index 00000000..7b0f5ca0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/WallGroup.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group } from 'react-konva' +import Shapes from '../../../../shapes/index' +import { deriveWallLocations } from '../../../../util/tile-calculations' +import WallSegment from '../elements/WallSegment' + +const WallGroup = ({ tiles }) => { + return ( + <Group> + {deriveWallLocations(tiles).map((wallSegment, index) => ( + <WallSegment key={index} wallSegment={wallSegment} /> + ))} + </Group> + ) +} + +WallGroup.propTypes = { + tiles: PropTypes.arrayOf(Shapes.Tile).isRequired, +} + +export default WallGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js new file mode 100644 index 00000000..bead87de --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Layer } from 'react-konva' +import HoverTile from '../elements/HoverTile' +import { TILE_SIZE_IN_PIXELS } from '../MapConstants' + +class HoverLayerComponent extends React.Component { + static propTypes = { + mouseX: PropTypes.number.isRequired, + mouseY: PropTypes.number.isRequired, + mapPosition: PropTypes.object.isRequired, + mapScale: PropTypes.number.isRequired, + isEnabled: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + } + + state = { + positionX: -1, + positionY: -1, + validity: false, + } + + componentDidUpdate() { + if (!this.props.isEnabled()) { + return + } + + const positionX = Math.floor( + (this.props.mouseX - this.props.mapPosition.x) / (this.props.mapScale * TILE_SIZE_IN_PIXELS) + ) + const positionY = Math.floor( + (this.props.mouseY - this.props.mapPosition.y) / (this.props.mapScale * TILE_SIZE_IN_PIXELS) + ) + + if (positionX !== this.state.positionX || positionY !== this.state.positionY) { + this.setState({ + positionX, + positionY, + validity: this.props.isValid(positionX, positionY), + }) + } + } + + render() { + if (!this.props.isEnabled()) { + return <Layer /> + } + + const pixelX = this.props.mapScale * this.state.positionX * TILE_SIZE_IN_PIXELS + this.props.mapPosition.x + const pixelY = this.props.mapScale * this.state.positionY * TILE_SIZE_IN_PIXELS + this.props.mapPosition.y + + return ( + <Layer opacity={0.6}> + <HoverTile + pixelX={pixelX} + pixelY={pixelY} + scale={this.props.mapScale} + isValid={this.state.validity} + onClick={() => + this.state.validity ? this.props.onClick(this.state.positionX, this.state.positionY) : undefined + } + /> + {this.props.children + ? React.cloneElement(this.props.children, { + pixelX, + pixelY, + scale: this.props.mapScale, + }) + : undefined} + </Layer> + ) + } +} + +export default HoverLayerComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js new file mode 100644 index 00000000..8ee14c9c --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js @@ -0,0 +1,17 @@ +import React from 'react' +import { Group, Layer } from 'react-konva' +import TopologyContainer from '../../../../containers/app/map/TopologyContainer' +import Backdrop from '../elements/Backdrop' +import GridGroup from '../groups/GridGroup' + +const MapLayerComponent = ({ mapPosition, mapScale }) => ( + <Layer> + <Group x={mapPosition.x} y={mapPosition.y} scaleX={mapScale} scaleY={mapScale}> + <Backdrop /> + <TopologyContainer /> + <GridGroup /> + </Group> + </Layer> +) + +export default MapLayerComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js new file mode 100644 index 00000000..661fc255 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js @@ -0,0 +1,11 @@ +import React from 'react' +import TilePlusIcon from '../elements/TilePlusIcon' +import HoverLayerComponent from './HoverLayerComponent' + +const ObjectHoverLayerComponent = (props) => ( + <HoverLayerComponent {...props}> + <TilePlusIcon {...props} /> + </HoverLayerComponent> +) + +export default ObjectHoverLayerComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js new file mode 100644 index 00000000..887e2891 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js @@ -0,0 +1,6 @@ +import React from 'react' +import HoverLayerComponent from './HoverLayerComponent' + +const RoomHoverLayerComponent = (props) => <HoverLayerComponent {...props} /> + +export default RoomHoverLayerComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js new file mode 100644 index 00000000..759acd57 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js @@ -0,0 +1,93 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Bar, CartesianGrid, ComposedChart, ErrorBar, ResponsiveContainer, Scatter, XAxis, YAxis } from 'recharts' +import { AVAILABLE_METRICS, METRIC_NAMES_SHORT, METRIC_UNITS } from '../../../util/available-metrics' +import { mean, std } from 'mathjs' +import Shapes from '../../../shapes/index' +import approx from 'approximate-number' + +const PortfolioResultsComponent = ({ portfolio, scenarios }) => { + if (!portfolio) { + return <div>Loading...</div> + } + + const nonFinishedScenarios = scenarios.filter((s) => s.simulation.state !== 'FINISHED') + + if (nonFinishedScenarios.length > 0) { + if (nonFinishedScenarios.every((s) => s.simulation.state === 'QUEUED' || s.simulation.state === 'RUNNING')) { + return ( + <div> + <h1>Simulation running...</h1> + <p>{nonFinishedScenarios.length} of the scenarios are still being simulated</p> + </div> + ) + } + if (nonFinishedScenarios.some((s) => s.simulation.state === 'FAILED')) { + return ( + <div> + <h1>Simulation failed.</h1> + <p> + Try again by creating a new scenario. Please contact the OpenDC team for support, if issues + persist. + </p> + </div> + ) + } + } + + const dataPerMetric = {} + + AVAILABLE_METRICS.forEach((metric) => { + dataPerMetric[metric] = scenarios.map((scenario) => ({ + name: scenario.name, + value: mean(scenario.results[metric]), + errorX: std(scenario.results[metric]), + })) + }) + + return ( + <div className="full-height" style={{ overflowY: 'scroll', overflowX: 'hidden' }}> + <h2>Portfolio: {portfolio.name}</h2> + <p>Repeats per Scenario: {portfolio.targets.repeatsPerScenario}</p> + <div className="row"> + {AVAILABLE_METRICS.map((metric) => ( + <div className="col-6 mb-2" key={metric}> + <h4>{METRIC_NAMES_SHORT[metric]}</h4> + <ResponsiveContainer aspect={16 / 9} width="100%"> + <ComposedChart + data={dataPerMetric[metric]} + margin={{ left: 35, bottom: 15 }} + layout="vertical" + > + <CartesianGrid strokeDasharray="3 3" /> + <XAxis + tickFormatter={(tick) => approx(tick)} + label={{ value: METRIC_UNITS[metric], position: 'bottom', offset: 0 }} + type="number" + /> + <YAxis dataKey="name" type="category" /> + <Bar dataKey="value" fill="#3399FF" isAnimationActive={false} /> + <Scatter dataKey="value" opacity={0} isAnimationActive={false}> + <ErrorBar + dataKey="errorX" + width={10} + strokeWidth={3} + stroke="#FF6600" + direction="x" + /> + </Scatter> + </ComposedChart> + </ResponsiveContainer> + </div> + ))} + </div> + </div> + ) +} + +PortfolioResultsComponent.propTypes = { + portfolio: Shapes.Portfolio, + scenarios: PropTypes.arrayOf(Shapes.Scenario), +} + +export default PortfolioResultsComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js new file mode 100644 index 00000000..f7368f54 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js @@ -0,0 +1,53 @@ +import PropTypes from 'prop-types' +import classNames from 'classnames' +import React from 'react' +import './Sidebar.sass' + +class Sidebar extends React.Component { + static propTypes = { + isRight: PropTypes.bool.isRequired, + collapsible: PropTypes.bool, + } + + static defaultProps = { + collapsible: true, + } + + state = { + collapsed: false, + } + + render() { + const collapseButton = ( + <div + className={classNames('sidebar-collapse-button', { + 'sidebar-collapse-button-right': this.props.isRight, + })} + onClick={() => this.setState({ collapsed: !this.state.collapsed })} + > + {(this.state.collapsed && this.props.isRight) || (!this.state.collapsed && !this.props.isRight) ? ( + <span className="fa fa-angle-left" title={this.props.isRight ? 'Expand' : 'Collapse'} /> + ) : ( + <span className="fa fa-angle-right" title={this.props.isRight ? 'Collapse' : 'Expand'} /> + )} + </div> + ) + + if (this.state.collapsed) { + return collapseButton + } + return ( + <div + className={classNames('sidebar p-3 h-100', { + 'sidebar-right': this.props.isRight, + })} + onWheel={(e) => e.stopPropagation()} + > + {this.props.children} + {this.props.collapsible && collapseButton} + </div> + ) + } +} + +export default Sidebar diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.sass b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.sass new file mode 100644 index 00000000..b8e15716 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.sass @@ -0,0 +1,50 @@ +@import ../../../style-globals/_variables.sass +@import ../../../style-globals/_mixins.sass + +.sidebar-collapse-button + position: absolute + left: 5px + top: 5px + padding: 5px 7px + + background: white + border: solid 1px $gray-semi-light + z-index: 99 + + +clickable + +border-radius(5px) + +transition(background, 200ms) + + &.sidebar-collapse-button-right + left: auto + right: 5px + top: 5px + + &:hover + background: #eeeeee + +.sidebar + position: absolute + top: 0 + left: 0 + width: $side-bar-width + + z-index: 100 + background: white + + border-right: $gray-semi-dark 1px solid + + .sidebar-collapse-button + left: auto + right: -25px + +.sidebar-right + left: auto + right: 0 + + border-left: $gray-semi-dark 1px solid + border-right: none + + .sidebar-collapse-button-right + left: -25px + right: auto diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js new file mode 100644 index 00000000..b000b9e2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js @@ -0,0 +1,66 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Shapes from '../../../../shapes' +import { Link } from 'react-router-dom' +import FontAwesome from 'react-fontawesome' +import ScenarioListContainer from '../../../../containers/app/sidebars/project/ScenarioListContainer' + +class PortfolioListComponent extends React.Component { + static propTypes = { + portfolios: PropTypes.arrayOf(Shapes.Portfolio), + currentProjectId: PropTypes.string.isRequired, + currentPortfolioId: PropTypes.string, + onNewPortfolio: PropTypes.func.isRequired, + onChoosePortfolio: PropTypes.func.isRequired, + onDeletePortfolio: PropTypes.func.isRequired, + } + + onDelete(id) { + this.props.onDeletePortfolio(id) + } + + render() { + return ( + <div className="pb-3"> + <h2> + Portfolios + <button + className="btn btn-outline-primary float-right" + onClick={this.props.onNewPortfolio.bind(this)} + > + <FontAwesome name="plus" /> + </button> + </h2> + + {this.props.portfolios.map((portfolio, idx) => ( + <div key={portfolio._id}> + <div className="row mb-1"> + <div + className={ + 'col-7 align-self-center ' + + (portfolio._id === this.props.currentPortfolioId ? 'font-weight-bold' : '') + } + > + {portfolio.name} + </div> + <div className="col-5 text-right"> + <Link + className="btn btn-outline-primary mr-1 fa fa-play" + to={`/projects/${this.props.currentProjectId}/portfolios/${portfolio._id}`} + onClick={() => this.props.onChoosePortfolio(portfolio._id)} + /> + <span + className="btn btn-outline-danger fa fa-trash" + onClick={() => this.onDelete(portfolio._id)} + /> + </div> + </div> + <ScenarioListContainer portfolioId={portfolio._id} /> + </div> + ))} + </div> + ) + } +} + +export default PortfolioListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js new file mode 100644 index 00000000..4789315e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js @@ -0,0 +1,15 @@ +import React from 'react' +import Sidebar from '../Sidebar' +import TopologyListContainer from '../../../../containers/app/sidebars/project/TopologyListContainer' +import PortfolioListContainer from '../../../../containers/app/sidebars/project/PortfolioListContainer' + +const ProjectSidebarComponent = ({ collapsible }) => ( + <Sidebar isRight={false} collapsible={collapsible}> + <div className="h-100 overflow-auto container-fluid"> + <TopologyListContainer /> + <PortfolioListContainer /> + </div> + </Sidebar> +) + +export default ProjectSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js new file mode 100644 index 00000000..e775a663 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Shapes from '../../../../shapes' +import { Link } from 'react-router-dom' +import FontAwesome from 'react-fontawesome' + +class ScenarioListComponent extends React.Component { + static propTypes = { + scenarios: PropTypes.arrayOf(Shapes.Scenario), + portfolioId: PropTypes.string, + currentProjectId: PropTypes.string.isRequired, + currentScenarioId: PropTypes.string, + onNewScenario: PropTypes.func.isRequired, + onChooseScenario: PropTypes.func.isRequired, + onDeleteScenario: PropTypes.func.isRequired, + } + + onDelete(id) { + this.props.onDeleteScenario(id) + } + + render() { + return ( + <> + {this.props.scenarios.map((scenario, idx) => ( + <div key={scenario._id} className="row mb-1"> + <div + className={ + 'col-7 pl-5 align-self-center ' + + (scenario._id === this.props.currentScenarioId ? 'font-weight-bold' : '') + } + > + {scenario.name} + </div> + <div className="col-5 text-right"> + <Link + className="btn btn-outline-primary mr-1 fa fa-play disabled" + to={`/projects/${this.props.currentProjectId}/portfolios/${scenario.portfolioId}/scenarios/${scenario._id}`} + onClick={() => this.props.onChooseScenario(scenario.portfolioId, scenario._id)} + /> + <span + className={'btn btn-outline-danger fa fa-trash ' + (idx === 0 ? 'disabled' : '')} + onClick={() => (idx !== 0 ? this.onDelete(scenario._id) : undefined)} + /> + </div> + </div> + ))} + <div className="pl-4 mb-2"> + <div + className="btn btn-outline-primary" + onClick={() => this.props.onNewScenario(this.props.portfolioId)} + > + <FontAwesome name="plus" className="mr-1" /> + New scenario + </div> + </div> + </> + ) + } +} + +export default ScenarioListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js new file mode 100644 index 00000000..2f42f7e4 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Shapes from '../../../../shapes' +import FontAwesome from 'react-fontawesome' + +class TopologyListComponent extends React.Component { + static propTypes = { + topologies: PropTypes.arrayOf(Shapes.Topology), + currentTopologyId: PropTypes.string, + onChooseTopology: PropTypes.func.isRequired, + onNewTopology: PropTypes.func.isRequired, + onDeleteTopology: PropTypes.func.isRequired, + } + + onChoose(id) { + this.props.onChooseTopology(id) + } + + onDelete(id) { + this.props.onDeleteTopology(id) + } + + render() { + return ( + <div className="pb-3"> + <h2> + Topologies + <button className="btn btn-outline-primary float-right" onClick={this.props.onNewTopology}> + <FontAwesome name="plus" /> + </button> + </h2> + + {this.props.topologies.map((topology, idx) => ( + <div key={topology._id} className="row mb-1"> + <div + className={ + 'col-7 align-self-center ' + + (topology._id === this.props.currentTopologyId ? 'font-weight-bold' : '') + } + > + {topology.name} + </div> + <div className="col-5 text-right"> + <span + className="btn btn-outline-primary mr-1 fa fa-play" + onClick={() => this.onChoose(topology._id)} + /> + <span + className={'btn btn-outline-danger fa fa-trash ' + (idx === 0 ? 'disabled' : '')} + onClick={() => (idx !== 0 ? this.onDelete(topology._id) : undefined)} + /> + </div> + </div> + ))} + </div> + ) + } +} + +export default TopologyListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js new file mode 100644 index 00000000..5fb0dc55 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js @@ -0,0 +1,13 @@ +import React from 'react' +import FontAwesome from 'react-fontawesome' + +const NameComponent = ({ name, onEdit }) => ( + <h2> + {name} + <button className="btn btn-outline-secondary float-right" onClick={onEdit}> + <FontAwesome name="pencil" /> + </button> + </h2> +) + +export default NameComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js new file mode 100644 index 00000000..f5eee36b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js @@ -0,0 +1,31 @@ +import React from 'react' +import BuildingSidebarContainer from '../../../../containers/app/sidebars/topology/building/BuildingSidebarContainer' +import MachineSidebarContainer from '../../../../containers/app/sidebars/topology/machine/MachineSidebarContainer' +import RackSidebarContainer from '../../../../containers/app/sidebars/topology/rack/RackSidebarContainer' +import RoomSidebarContainer from '../../../../containers/app/sidebars/topology/room/RoomSidebarContainer' +import Sidebar from '../Sidebar' + +const TopologySidebarComponent = ({ interactionLevel }) => { + let sidebarContent + + switch (interactionLevel.mode) { + case 'BUILDING': + sidebarContent = <BuildingSidebarContainer /> + break + case 'ROOM': + sidebarContent = <RoomSidebarContainer /> + break + case 'RACK': + sidebarContent = <RackSidebarContainer /> + break + case 'MACHINE': + sidebarContent = <MachineSidebarContainer /> + break + default: + sidebarContent = 'Missing Content' + } + + return <Sidebar isRight={true}>{sidebarContent}</Sidebar> +} + +export default TopologySidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js new file mode 100644 index 00000000..eea62f84 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js @@ -0,0 +1,13 @@ +import React from 'react' +import NewRoomConstructionContainer from '../../../../../containers/app/sidebars/topology/building/NewRoomConstructionContainer' + +const BuildingSidebarComponent = () => { + return ( + <div> + <h2>Building</h2> + <NewRoomConstructionContainer /> + </div> + ) +} + +export default BuildingSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js new file mode 100644 index 00000000..fd552c1e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js @@ -0,0 +1,26 @@ +import React from 'react' + +const NewRoomConstructionComponent = ({ onStart, onFinish, onCancel, currentRoomInConstruction }) => { + if (currentRoomInConstruction === '-1') { + return ( + <div className="btn btn-outline-primary btn-block" onClick={onStart}> + <span className="fa fa-plus mr-2" /> + Construct a new room + </div> + ) + } + return ( + <div> + <div className="btn btn-primary btn-block" onClick={onFinish}> + <span className="fa fa-check mr-2" /> + Finalize new room + </div> + <div className="btn btn-default btn-block" onClick={onCancel}> + <span className="fa fa-times mr-2" /> + Cancel construction + </div> + </div> + ) +} + +export default NewRoomConstructionComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js new file mode 100644 index 00000000..70d522b2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const BackToRackComponent = ({ onClick }) => ( + <div className="btn btn-secondary btn-block" onClick={onClick}> + <span className="fa fa-angle-left mr-2" /> + Back to rack + </div> +) + +export default BackToRackComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js new file mode 100644 index 00000000..37820316 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const DeleteMachineComponent = ({ onClick }) => ( + <div className="btn btn-outline-danger btn-block" onClick={onClick}> + <span className="fa fa-trash mr-2" /> + Delete this machine + </div> +) + +export default DeleteMachineComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineNameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineNameComponent.js new file mode 100644 index 00000000..992383c4 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineNameComponent.js @@ -0,0 +1,5 @@ +import React from 'react' + +const MachineNameComponent = ({ position }) => <h2>Machine at slot {position}</h2> + +export default MachineNameComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js new file mode 100644 index 00000000..7c78cf9e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js @@ -0,0 +1,18 @@ +import React from 'react' +import BackToRackContainer from '../../../../../containers/app/sidebars/topology/machine/BackToRackContainer' +import DeleteMachineContainer from '../../../../../containers/app/sidebars/topology/machine/DeleteMachineContainer' +import MachineNameContainer from '../../../../../containers/app/sidebars/topology/machine/MachineNameContainer' +import UnitTabsContainer from '../../../../../containers/app/sidebars/topology/machine/UnitTabsContainer' + +const MachineSidebarComponent = ({ machineId }) => { + return ( + <div className="h-100 overflow-auto"> + <MachineNameContainer /> + <BackToRackContainer /> + <DeleteMachineContainer /> + <UnitTabsContainer /> + </div> + ) +} + +export default MachineSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js new file mode 100644 index 00000000..4e9dbc7e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types' +import React from 'react' + +class UnitAddComponent extends React.Component { + static propTypes = { + units: PropTypes.array.isRequired, + onAdd: PropTypes.func.isRequired, + } + + render() { + return ( + <div className="form-inline"> + <div className="form-group w-100"> + <select className="form-control w-70 mr-1" ref={(unitSelect) => (this.unitSelect = unitSelect)}> + {this.props.units.map((unit) => ( + <option value={unit._id} key={unit._id}> + {unit.name} + </option> + ))} + </select> + <button + type="submit" + className="btn btn-outline-primary" + onClick={() => this.props.onAdd(this.unitSelect.value)} + > + <span className="fa fa-plus mr-2" /> + Add + </button> + </div> + </div> + ) + } +} + +export default UnitAddComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js new file mode 100644 index 00000000..de55e506 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js @@ -0,0 +1,52 @@ +import React from 'react' +import { UncontrolledPopover, PopoverHeader, PopoverBody, Button } from 'reactstrap' + +function UnitComponent({ index, unitType, unit, onDelete }) { + let unitInfo + if (unitType === 'cpu' || unitType === 'gpu') { + unitInfo = ( + <> + <strong>Clockrate: </strong> + <code>{unit.clockRateMhz}</code> + <br /> + <strong>Num. Cores: </strong> + <code>{unit.numberOfCores}</code> + <br /> + <strong>Energy Cons.: </strong> + <code>{unit.energyConsumptionW} W</code> + <br /> + </> + ) + } else if (unitType === 'memory' || unitType === 'storage') { + unitInfo = ( + <> + <strong>Speed:</strong> + <code>{unit.speedMbPerS} Mb/s</code> + <br /> + <strong>Size:</strong> + <code>{unit.sizeMb} MB</code> + <br /> + <strong>Energy Cons.:</strong> + <code>{unit.energyConsumptionW} W</code> + <br /> + </> + ) + } + + return ( + <li className="d-flex list-group-item justify-content-between align-items-center"> + <span style={{ maxWidth: '60%' }}>{unit.name}</span> + <span> + <Button outline={true} color="info" className="mr-1 fa fa-info-circle" id={`unit-${index}`} /> + <UncontrolledPopover trigger="focus" placement="left" target={`unit-${index}`}> + <PopoverHeader>Unit Information</PopoverHeader> + <PopoverBody>{unitInfo}</PopoverBody> + </UncontrolledPopover> + + <span className="btn btn-outline-danger fa fa-trash" onClick={onDelete} /> + </span> + </li> + ) +} + +export default UnitComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js new file mode 100644 index 00000000..2ade0f6a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js @@ -0,0 +1,20 @@ +import React from 'react' +import UnitContainer from '../../../../../containers/app/sidebars/topology/machine/UnitContainer' + +const UnitListComponent = ({ unitType, unitIds }) => ( + <ul className="list-group mt-1"> + {unitIds.length !== 0 ? ( + unitIds.map((unitId, index) => ( + <UnitContainer unitType={unitType} unitId={unitId} index={index} key={index} /> + )) + ) : ( + <div className="alert alert-info"> + <span> + <strong>No units...</strong> Add some with the menu above! + </span> + </div> + )} + </ul> +) + +export default UnitListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js new file mode 100644 index 00000000..6599fefd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js @@ -0,0 +1,78 @@ +import React, { useState } from 'react' +import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap' +import UnitAddContainer from '../../../../../containers/app/sidebars/topology/machine/UnitAddContainer' +import UnitListContainer from '../../../../../containers/app/sidebars/topology/machine/UnitListContainer' + +const UnitTabsComponent = () => { + const [activeTab, setActiveTab] = useState('cpu-units') + const toggle = (tab) => { + if (activeTab !== tab) setActiveTab(tab) + } + + return ( + <div> + <Nav tabs> + <NavItem> + <NavLink + className={activeTab === 'cpu-units' ? 'active' : ''} + onClick={() => { + toggle('cpu-units') + }} + > + CPU + </NavLink> + </NavItem> + <NavItem> + <NavLink + className={activeTab === 'gpu-units' ? 'active' : ''} + onClick={() => { + toggle('gpu-units') + }} + > + GPU + </NavLink> + </NavItem> + <NavItem> + <NavLink + className={activeTab === 'memory-units' ? 'active' : ''} + onClick={() => { + toggle('memory-units') + }} + > + Memory + </NavLink> + </NavItem> + <NavItem> + <NavLink + className={activeTab === 'storage-units' ? 'active' : ''} + onClick={() => { + toggle('storage-units') + }} + > + Stor. + </NavLink> + </NavItem> + </Nav> + <TabContent activeTab={activeTab}> + <TabPane tabId="cpu-units"> + <UnitAddContainer unitType="cpu" /> + <UnitListContainer unitType="cpu" /> + </TabPane> + <TabPane tabId="gpu-units"> + <UnitAddContainer unitType="gpu" /> + <UnitListContainer unitType="gpu" /> + </TabPane> + <TabPane tabId="memory-units"> + <UnitAddContainer unitType="memory" /> + <UnitListContainer unitType="memory" /> + </TabPane> + <TabPane tabId="storage-units"> + <UnitAddContainer unitType="storage" /> + <UnitListContainer unitType="storage" /> + </TabPane> + </TabContent> + </div> + ) +} + +export default UnitTabsComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js new file mode 100644 index 00000000..75418f9d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const AddPrefabComponent = ({ onClick }) => ( + <div className="btn btn-primary btn-block" onClick={onClick}> + <span className="fa fa-floppy-o mr-2" /> + Save this rack to a prefab + </div> +) + +export default AddPrefabComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js new file mode 100644 index 00000000..c14775bf --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const BackToRoomComponent = ({ onClick }) => ( + <div className="btn btn-secondary btn-block mb-2" onClick={onClick}> + <span className="fa fa-angle-left mr-2" /> + Back to room + </div> +) + +export default BackToRoomComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackComponent.js new file mode 100644 index 00000000..23b0daac --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const DeleteRackComponent = ({ onClick }) => ( + <div className="btn btn-outline-danger btn-block" onClick={onClick}> + <span className="fa fa-trash mr-2" /> + Delete this rack + </div> +) + +export default DeleteRackComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js new file mode 100644 index 00000000..d7e30f1d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js @@ -0,0 +1,13 @@ +import React from 'react' + +const EmptySlotComponent = ({ position, onAdd }) => ( + <li className="list-group-item d-flex justify-content-between align-items-center"> + <span className="badge badge-default badge-info mr-1 disabled">{position}</span> + <button className="btn btn-outline-primary" onClick={onAdd}> + <span className="fa fa-plus mr-2" /> + Add machine + </button> + </li> +) + +export default EmptySlotComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js new file mode 100644 index 00000000..caa3dc04 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js @@ -0,0 +1,43 @@ +import React from 'react' +import Shapes from '../../../../../shapes' + +const UnitIcon = ({ id, type }) => ( + <div> + <img + src={'/img/topology/' + id + '-icon.png'} + alt={'Machine contains ' + type + ' units'} + className="img-fluid ml-1" + style={{ maxHeight: '35px' }} + /> + </div> +) + +const MachineComponent = ({ position, machine, onClick }) => { + const hasNoUnits = + machine.cpuIds.length + machine.gpuIds.length + machine.memoryIds.length + machine.storageIds.length === 0 + + return ( + <li + className="d-flex list-group-item list-group-item-action justify-content-between align-items-center" + onClick={onClick} + style={{ backgroundColor: 'white' }} + > + <span className="badge badge-default badge-info mr-1">{position}</span> + <div className="d-inline-flex"> + {machine.cpuIds.length > 0 ? <UnitIcon id="cpu" type="CPU" /> : undefined} + {machine.gpuIds.length > 0 ? <UnitIcon id="gpu" type="GPU" /> : undefined} + {machine.memoryIds.length > 0 ? <UnitIcon id="memory" type="memory" /> : undefined} + {machine.storageIds.length > 0 ? <UnitIcon id="storage" type="storage" /> : undefined} + {hasNoUnits ? ( + <span className="badge badge-default badge-warning">Machine with no units</span> + ) : undefined} + </div> + </li> + ) +} + +MachineComponent.propTypes = { + machine: Shapes.Machine, +} + +export default MachineComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js new file mode 100644 index 00000000..12be26bd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js @@ -0,0 +1,20 @@ +import React from 'react' +import EmptySlotContainer from '../../../../../containers/app/sidebars/topology/rack/EmptySlotContainer' +import MachineContainer from '../../../../../containers/app/sidebars/topology/rack/MachineContainer' +import './MachineListComponent.sass' + +const MachineListComponent = ({ machineIds }) => { + return ( + <ul className="list-group machine-list"> + {machineIds.map((machineId, index) => { + if (machineId === null) { + return <EmptySlotContainer key={index} position={index + 1} /> + } else { + return <MachineContainer key={index} position={index + 1} machineId={machineId} /> + } + })} + </ul> + ) +} + +export default MachineListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.sass b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.sass new file mode 100644 index 00000000..11b82c93 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.sass @@ -0,0 +1,2 @@ +.machine-list li + min-height: 64px diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameComponent.js new file mode 100644 index 00000000..b701909a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameComponent.js @@ -0,0 +1,6 @@ +import React from 'react' +import NameComponent from '../NameComponent' + +const RackNameComponent = ({ rackName, onEdit }) => <NameComponent name={rackName} onEdit={onEdit} /> + +export default RackNameComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js new file mode 100644 index 00000000..ca41bf57 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js @@ -0,0 +1,25 @@ +import React from 'react' +import BackToRoomContainer from '../../../../../containers/app/sidebars/topology/rack/BackToRoomContainer' +import DeleteRackContainer from '../../../../../containers/app/sidebars/topology/rack/DeleteRackContainer' +import MachineListContainer from '../../../../../containers/app/sidebars/topology/rack/MachineListContainer' +import RackNameContainer from '../../../../../containers/app/sidebars/topology/rack/RackNameContainer' +import './RackSidebarComponent.sass' +import AddPrefabContainer from '../../../../../containers/app/sidebars/topology/rack/AddPrefabContainer' + +const RackSidebarComponent = () => { + return ( + <div className="rack-sidebar-container flex-column"> + <div className="rack-sidebar-header-container"> + <RackNameContainer /> + <BackToRoomContainer /> + <AddPrefabContainer /> + <DeleteRackContainer /> + </div> + <div className="machine-list-container mt-2"> + <MachineListContainer /> + </div> + </div> + ) +} + +export default RackSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass new file mode 100644 index 00000000..29fec02a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass @@ -0,0 +1,11 @@ +.rack-sidebar-container + display: flex + height: 100% + max-height: 100% + +.rack-sidebar-header-container + flex: 0 + +.machine-list-container + flex: 1 + overflow-y: scroll diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js new file mode 100644 index 00000000..64c0a1f6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const BackToBuildingComponent = ({ onClick }) => ( + <div className="btn btn-secondary btn-block mb-2" onClick={onClick}> + <span className="fa fa-angle-left mr-2" /> + Back to building + </div> +) + +export default BackToBuildingComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js new file mode 100644 index 00000000..78417359 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const DeleteRoomComponent = ({ onClick }) => ( + <div className="btn btn-outline-danger btn-block" onClick={onClick}> + <span className="fa fa-trash mr-2" /> + Delete this room + </div> +) + +export default DeleteRoomComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomComponent.js new file mode 100644 index 00000000..857a646f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomComponent.js @@ -0,0 +1,22 @@ +import classNames from 'classnames' +import React from 'react' + +const EditRoomComponent = ({ onEdit, onFinish, isEditing, isInRackConstructionMode }) => + isEditing ? ( + <div className="btn btn-info btn-block" onClick={onFinish}> + <span className="fa fa-check mr-2" /> + Finish editing room + </div> + ) : ( + <div + className={classNames('btn btn-outline-info btn-block', { + disabled: isInRackConstructionMode, + })} + onClick={() => (isInRackConstructionMode ? undefined : onEdit())} + > + <span className="fa fa-pencil mr-2" /> + Edit the tiles of this room + </div> + ) + +export default EditRoomComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js new file mode 100644 index 00000000..44566f61 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js @@ -0,0 +1,27 @@ +import classNames from 'classnames' +import React from 'react' + +const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, isEditingRoom }) => { + if (inRackConstructionMode) { + return ( + <div className="btn btn-primary btn-block" onClick={onStop}> + <span className="fa fa-times mr-2" /> + Stop rack construction + </div> + ) + } + + return ( + <div + className={classNames('btn btn-outline-primary btn-block', { + disabled: isEditingRoom, + })} + onClick={() => (isEditingRoom ? undefined : onStart())} + > + <span className="fa fa-plus mr-2" /> + Start rack construction + </div> + ) +} + +export default RackConstructionComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomNameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomNameComponent.js new file mode 100644 index 00000000..d637828e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomNameComponent.js @@ -0,0 +1,6 @@ +import React from 'react' +import NameComponent from '../NameComponent' + +const RoomNameComponent = ({ roomName, onEdit }) => <NameComponent name={roomName} onEdit={onEdit} /> + +export default RoomNameComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js new file mode 100644 index 00000000..1bc6533e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js @@ -0,0 +1,20 @@ +import React from 'react' +import BackToBuildingContainer from '../../../../../containers/app/sidebars/topology/room/BackToBuildingContainer' +import DeleteRoomContainer from '../../../../../containers/app/sidebars/topology/room/DeleteRoomContainer' +import EditRoomContainer from '../../../../../containers/app/sidebars/topology/room/EditRoomContainer' +import RackConstructionContainer from '../../../../../containers/app/sidebars/topology/room/RackConstructionContainer' +import RoomNameContainer from '../../../../../containers/app/sidebars/topology/room/RoomNameContainer' + +const RoomSidebarComponent = () => { + return ( + <div> + <RoomNameContainer /> + <BackToBuildingContainer /> + <RackConstructionContainer /> + <EditRoomContainer /> + <DeleteRoomContainer /> + </div> + ) +} + +export default RoomSidebarComponent |
