From bf7708f658cc6299a3b775afe24459b5a808c54d Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Fri, 22 Sep 2017 21:20:54 +0200 Subject: Restructure component and container directories --- src/components/app/map/LoadingScreen.js | 11 ++ src/components/app/map/MapConstants.js | 28 +++++ src/components/app/map/MapStageComponent.js | 135 +++++++++++++++++++++ .../app/map/controls/ExportCanvasComponent.js | 13 ++ .../app/map/controls/ScaleIndicatorComponent.js | 14 +++ .../app/map/controls/ScaleIndicatorComponent.sass | 9 ++ .../app/map/controls/ToolPanelComponent.js | 13 ++ .../app/map/controls/ToolPanelComponent.sass | 5 + .../app/map/controls/ZoomControlComponent.js | 31 +++++ src/components/app/map/elements/Backdrop.js | 16 +++ src/components/app/map/elements/GrayLayer.js | 17 +++ src/components/app/map/elements/HoverTile.js | 27 +++++ src/components/app/map/elements/ImageComponent.js | 48 ++++++++ src/components/app/map/elements/RackFillBar.js | 67 ++++++++++ src/components/app/map/elements/RoomTile.js | 20 +++ src/components/app/map/elements/TileObject.js | 25 ++++ src/components/app/map/elements/TilePlusIcon.js | 44 +++++++ src/components/app/map/elements/WallSegment.js | 39 ++++++ src/components/app/map/groups/DatacenterGroup.js | 42 +++++++ src/components/app/map/groups/GridGroup.js | 30 +++++ src/components/app/map/groups/RackGroup.js | 34 ++++++ src/components/app/map/groups/RoomGroup.js | 48 ++++++++ src/components/app/map/groups/TileGroup.js | 42 +++++++ src/components/app/map/groups/WallGroup.js | 22 ++++ .../app/map/layers/HoverLayerComponent.js | 63 ++++++++++ src/components/app/map/layers/MapLayerComponent.js | 17 +++ .../app/map/layers/ObjectHoverLayerComponent.js | 11 ++ .../app/map/layers/RoomHoverLayerComponent.js | 8 ++ src/components/app/sidebars/Sidebar.js | 38 ++++++ src/components/app/sidebars/Sidebar.sass | 50 ++++++++ .../app/sidebars/elements/LoadBarComponent.js | 22 ++++ .../app/sidebars/elements/LoadChartComponent.js | 40 ++++++ .../simulation/ExperimentMetadataComponent.js | 12 ++ .../app/sidebars/simulation/LoadMetricComponent.js | 33 +++++ .../simulation/SimulationSidebarComponent.js | 22 ++++ .../simulation/SimulationSidebarComponent.sass | 8 ++ .../app/sidebars/simulation/TaskComponent.js | 42 +++++++ .../app/sidebars/simulation/TraceComponent.js | 20 +++ .../app/sidebars/topology/NameComponent.js | 13 ++ .../sidebars/topology/TopologySidebarComponent.js | 35 ++++++ .../topology/building/BuildingSidebarComponent.js | 19 +++ .../building/NewRoomConstructionComponent.js | 27 +++++ .../topology/machine/BackToRackComponent.js | 10 ++ .../topology/machine/DeleteMachineComponent.js | 10 ++ .../topology/machine/MachineNameComponent.js | 7 ++ .../topology/machine/MachineSidebarComponent.js | 26 ++++ .../sidebars/topology/machine/UnitAddComponent.js | 38 ++++++ .../app/sidebars/topology/machine/UnitComponent.js | 16 +++ .../sidebars/topology/machine/UnitListComponent.js | 20 +++ .../sidebars/topology/machine/UnitTabsComponent.js | 54 +++++++++ .../sidebars/topology/rack/BackToRoomComponent.js | 10 ++ .../sidebars/topology/rack/DeleteRackComponent.js | 10 ++ .../sidebars/topology/rack/EmptySlotComponent.js | 20 +++ .../app/sidebars/topology/rack/MachineComponent.js | 65 ++++++++++ .../sidebars/topology/rack/MachineListComponent.js | 20 +++ .../topology/rack/MachineListComponent.sass | 2 + .../sidebars/topology/rack/RackNameComponent.js | 8 ++ .../sidebars/topology/rack/RackSidebarComponent.js | 33 +++++ .../topology/rack/RackSidebarComponent.sass | 11 ++ .../topology/room/BackToBuildingComponent.js | 10 ++ .../sidebars/topology/room/DeleteRoomComponent.js | 10 ++ .../topology/room/RackConstructionComponent.js | 21 ++++ .../sidebars/topology/room/RoomNameComponent.js | 8 ++ .../sidebars/topology/room/RoomSidebarComponent.js | 35 ++++++ .../sidebars/topology/room/RoomTypeComponent.js | 10 ++ src/components/app/timeline/PlayButtonComponent.js | 12 ++ src/components/app/timeline/Timeline.sass | 112 +++++++++++++++++ src/components/app/timeline/TimelineComponent.js | 37 ++++++ .../app/timeline/TimelineControlsComponent.js | 39 ++++++ .../app/timeline/TimelineLabelsComponent.js | 11 ++ 70 files changed, 1925 insertions(+) create mode 100644 src/components/app/map/LoadingScreen.js create mode 100644 src/components/app/map/MapConstants.js create mode 100644 src/components/app/map/MapStageComponent.js create mode 100644 src/components/app/map/controls/ExportCanvasComponent.js create mode 100644 src/components/app/map/controls/ScaleIndicatorComponent.js create mode 100644 src/components/app/map/controls/ScaleIndicatorComponent.sass create mode 100644 src/components/app/map/controls/ToolPanelComponent.js create mode 100644 src/components/app/map/controls/ToolPanelComponent.sass create mode 100644 src/components/app/map/controls/ZoomControlComponent.js create mode 100644 src/components/app/map/elements/Backdrop.js create mode 100644 src/components/app/map/elements/GrayLayer.js create mode 100644 src/components/app/map/elements/HoverTile.js create mode 100644 src/components/app/map/elements/ImageComponent.js create mode 100644 src/components/app/map/elements/RackFillBar.js create mode 100644 src/components/app/map/elements/RoomTile.js create mode 100644 src/components/app/map/elements/TileObject.js create mode 100644 src/components/app/map/elements/TilePlusIcon.js create mode 100644 src/components/app/map/elements/WallSegment.js create mode 100644 src/components/app/map/groups/DatacenterGroup.js create mode 100644 src/components/app/map/groups/GridGroup.js create mode 100644 src/components/app/map/groups/RackGroup.js create mode 100644 src/components/app/map/groups/RoomGroup.js create mode 100644 src/components/app/map/groups/TileGroup.js create mode 100644 src/components/app/map/groups/WallGroup.js create mode 100644 src/components/app/map/layers/HoverLayerComponent.js create mode 100644 src/components/app/map/layers/MapLayerComponent.js create mode 100644 src/components/app/map/layers/ObjectHoverLayerComponent.js create mode 100644 src/components/app/map/layers/RoomHoverLayerComponent.js create mode 100644 src/components/app/sidebars/Sidebar.js create mode 100644 src/components/app/sidebars/Sidebar.sass create mode 100644 src/components/app/sidebars/elements/LoadBarComponent.js create mode 100644 src/components/app/sidebars/elements/LoadChartComponent.js create mode 100644 src/components/app/sidebars/simulation/ExperimentMetadataComponent.js create mode 100644 src/components/app/sidebars/simulation/LoadMetricComponent.js create mode 100644 src/components/app/sidebars/simulation/SimulationSidebarComponent.js create mode 100644 src/components/app/sidebars/simulation/SimulationSidebarComponent.sass create mode 100644 src/components/app/sidebars/simulation/TaskComponent.js create mode 100644 src/components/app/sidebars/simulation/TraceComponent.js create mode 100644 src/components/app/sidebars/topology/NameComponent.js create mode 100644 src/components/app/sidebars/topology/TopologySidebarComponent.js create mode 100644 src/components/app/sidebars/topology/building/BuildingSidebarComponent.js create mode 100644 src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js create mode 100644 src/components/app/sidebars/topology/machine/BackToRackComponent.js create mode 100644 src/components/app/sidebars/topology/machine/DeleteMachineComponent.js create mode 100644 src/components/app/sidebars/topology/machine/MachineNameComponent.js create mode 100644 src/components/app/sidebars/topology/machine/MachineSidebarComponent.js create mode 100644 src/components/app/sidebars/topology/machine/UnitAddComponent.js create mode 100644 src/components/app/sidebars/topology/machine/UnitComponent.js create mode 100644 src/components/app/sidebars/topology/machine/UnitListComponent.js create mode 100644 src/components/app/sidebars/topology/machine/UnitTabsComponent.js create mode 100644 src/components/app/sidebars/topology/rack/BackToRoomComponent.js create mode 100644 src/components/app/sidebars/topology/rack/DeleteRackComponent.js create mode 100644 src/components/app/sidebars/topology/rack/EmptySlotComponent.js create mode 100644 src/components/app/sidebars/topology/rack/MachineComponent.js create mode 100644 src/components/app/sidebars/topology/rack/MachineListComponent.js create mode 100644 src/components/app/sidebars/topology/rack/MachineListComponent.sass create mode 100644 src/components/app/sidebars/topology/rack/RackNameComponent.js create mode 100644 src/components/app/sidebars/topology/rack/RackSidebarComponent.js create mode 100644 src/components/app/sidebars/topology/rack/RackSidebarComponent.sass create mode 100644 src/components/app/sidebars/topology/room/BackToBuildingComponent.js create mode 100644 src/components/app/sidebars/topology/room/DeleteRoomComponent.js create mode 100644 src/components/app/sidebars/topology/room/RackConstructionComponent.js create mode 100644 src/components/app/sidebars/topology/room/RoomNameComponent.js create mode 100644 src/components/app/sidebars/topology/room/RoomSidebarComponent.js create mode 100644 src/components/app/sidebars/topology/room/RoomTypeComponent.js create mode 100644 src/components/app/timeline/PlayButtonComponent.js create mode 100644 src/components/app/timeline/Timeline.sass create mode 100644 src/components/app/timeline/TimelineComponent.js create mode 100644 src/components/app/timeline/TimelineControlsComponent.js create mode 100644 src/components/app/timeline/TimelineLabelsComponent.js (limited to 'src/components/app') diff --git a/src/components/app/map/LoadingScreen.js b/src/components/app/map/LoadingScreen.js new file mode 100644 index 00000000..3d5753e2 --- /dev/null +++ b/src/components/app/map/LoadingScreen.js @@ -0,0 +1,11 @@ +import React from 'react'; +import FontAwesome from "react-fontawesome"; + +const LoadingScreen = () => ( +
+ + Loading your datacenter... +
+); + +export default LoadingScreen; diff --git a/src/components/app/map/MapConstants.js b/src/components/app/map/MapConstants.js new file mode 100644 index 00000000..a0166d15 --- /dev/null +++ b/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 = 4; +export const DEFAULT_RACK_SLOT_CAPACITY = 42; +export const DEFAULT_RACK_POWER_CAPACITY = 10000; diff --git a/src/components/app/map/MapStageComponent.js b/src/components/app/map/MapStageComponent.js new file mode 100644 index 00000000..271ae64f --- /dev/null +++ b/src/components/app/map/MapStageComponent.js @@ -0,0 +1,135 @@ +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 jQuery from "../../../util/jquery"; +import {NAVBAR_HEIGHT} from "../../navigation/Navbar"; +import { + MAP_MAX_SCALE, + MAP_MIN_SCALE, + MAP_MOVE_PIXELS_PER_EVENT, + MAP_SCALE_PER_EVENT, + MAP_SIZE_IN_PIXELS +} from "./MapConstants"; + +class MapStageComponent extends React.Component { + state = { + mouseX: 0, + mouseY: 0 + }; + + constructor() { + super(); + + this.updateDimensions = this.updateDimensions.bind(this); + this.updateScale = this.updateScale.bind(this); + } + + componentWillMount() { + this.updateDimensions(); + } + + componentDidMount() { + window.addEventListener("resize", this.updateDimensions); + window.addEventListener("wheel", this.updateScale); + + window["exportCanvasToImage"] = () => { + const canvasData = this.stage.getStage().toDataURL(); + const newWindow = window.open('about:blank', 'OpenDC Canvas Export'); + newWindow.document.write("Canvas Image Export"); + newWindow.document.title = "OpenDC Canvas Export"; + } + } + + componentWillUnmount() { + window.removeEventListener("resize", this.updateDimensions); + window.removeEventListener("wheel", this.updateScale); + } + + updateDimensions() { + this.props.setMapDimensions(jQuery(window).width(), jQuery(window).height() - NAVBAR_HEIGHT); + } + + updateScale(e) { + e.preventDefault(); + const mousePointsTo = { + x: this.state.mouseX / this.props.mapScale - this.props.mapPosition.x / this.props.mapScale, + y: this.state.mouseY / this.props.mapScale - this.props.mapPosition.y / this.props.mapScale, + }; + const newScale = e.deltaY < 0 ? this.props.mapScale * MAP_SCALE_PER_EVENT : this.props.mapScale / MAP_SCALE_PER_EVENT; + const boundedScale = Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE); + + const newX = -(mousePointsTo.x - this.state.mouseX / boundedScale) * boundedScale; + const newY = -(mousePointsTo.y - this.state.mouseY / boundedScale) * boundedScale; + + this.setPositionWithBoundsCheck(newX, newY); + this.props.setMapScale(boundedScale); + } + + 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.setPositionWithBoundsCheck(this.props.mapPosition.x + deltaX, this.props.mapPosition.y + deltaY); + } + + setPositionWithBoundsCheck(newX, newY) { + const scaledMapSize = MAP_SIZE_IN_PIXELS * this.props.mapScale; + const updatedX = newX > 0 ? 0 : + (newX < -scaledMapSize + this.props.mapDimensions.width + ? -scaledMapSize + this.props.mapDimensions.width : newX); + const updatedY = newY > 0 ? 0 : + (newY < -scaledMapSize + this.props.mapDimensions.height + ? -scaledMapSize + this.props.mapDimensions.height : newY); + + this.props.setMapPosition(updatedX, updatedY); + } + + render() { + return ( + + {this.stage = stage;}} + width={this.props.mapDimensions.width} + height={this.props.mapDimensions.height} + onMouseMove={this.updateMousePosition.bind(this)} + > + + + + + + ) + } +} + +export default MapStageComponent; diff --git a/src/components/app/map/controls/ExportCanvasComponent.js b/src/components/app/map/controls/ExportCanvasComponent.js new file mode 100644 index 00000000..2f044ffe --- /dev/null +++ b/src/components/app/map/controls/ExportCanvasComponent.js @@ -0,0 +1,13 @@ +import React from "react"; + +const ExportCanvasComponent = () => ( + +); + +export default ExportCanvasComponent; diff --git a/src/components/app/map/controls/ScaleIndicatorComponent.js b/src/components/app/map/controls/ScaleIndicatorComponent.js new file mode 100644 index 00000000..fd9483b5 --- /dev/null +++ b/src/components/app/map/controls/ScaleIndicatorComponent.js @@ -0,0 +1,14 @@ +import React from "react"; +import {TILE_SIZE_IN_METERS, TILE_SIZE_IN_PIXELS} from "../MapConstants"; +import "./ScaleIndicatorComponent.css"; + +const ScaleIndicatorComponent = ({scale}) => ( +
+ {TILE_SIZE_IN_METERS}m +
+); + +export default ScaleIndicatorComponent; diff --git a/src/components/app/map/controls/ScaleIndicatorComponent.sass b/src/components/app/map/controls/ScaleIndicatorComponent.sass new file mode 100644 index 00000000..f2d2b55b --- /dev/null +++ b/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/src/components/app/map/controls/ToolPanelComponent.js b/src/components/app/map/controls/ToolPanelComponent.js new file mode 100644 index 00000000..a065358a --- /dev/null +++ b/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.css"; + +const ToolPanelComponent = () => ( +
+ + +
+); + +export default ToolPanelComponent; diff --git a/src/components/app/map/controls/ToolPanelComponent.sass b/src/components/app/map/controls/ToolPanelComponent.sass new file mode 100644 index 00000000..996712b3 --- /dev/null +++ b/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/src/components/app/map/controls/ZoomControlComponent.js b/src/components/app/map/controls/ZoomControlComponent.js new file mode 100644 index 00000000..c5628d16 --- /dev/null +++ b/src/components/app/map/controls/ZoomControlComponent.js @@ -0,0 +1,31 @@ +import React from "react"; +import {MAP_MAX_SCALE, MAP_MIN_SCALE, MAP_SCALE_PER_EVENT} from "../MapConstants"; + +const ZoomControlComponent = ({mapScale, setMapScale}) => { + const zoom = (out) => { + const newScale = out ? mapScale / MAP_SCALE_PER_EVENT : mapScale * MAP_SCALE_PER_EVENT; + const boundedScale = Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE); + setMapScale(boundedScale); + }; + + return ( + + + + + ); +}; + +export default ZoomControlComponent; diff --git a/src/components/app/map/elements/Backdrop.js b/src/components/app/map/elements/Backdrop.js new file mode 100644 index 00000000..9c01df63 --- /dev/null +++ b/src/components/app/map/elements/Backdrop.js @@ -0,0 +1,16 @@ +import React from "react"; +import {Rect} from "react-konva"; +import {BACKDROP_COLOR} from "../../../../util/colors"; +import {MAP_SIZE_IN_PIXELS} from "../MapConstants"; + +const Backdrop = () => ( + +); + +export default Backdrop; diff --git a/src/components/app/map/elements/GrayLayer.js b/src/components/app/map/elements/GrayLayer.js new file mode 100644 index 00000000..c5994d06 --- /dev/null +++ b/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}) => ( + +); + +export default GrayLayer; diff --git a/src/components/app/map/elements/HoverTile.js b/src/components/app/map/elements/HoverTile.js new file mode 100644 index 00000000..fc12cbdd --- /dev/null +++ b/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}) => ( + +); + +HoverTile.propTypes = { + pixelX: PropTypes.number.isRequired, + pixelY: PropTypes.number.isRequired, + isValid: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, +}; + +export default HoverTile; diff --git a/src/components/app/map/elements/ImageComponent.js b/src/components/app/map/elements/ImageComponent.js new file mode 100644 index 00000000..486296ea --- /dev/null +++ b/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 ( + + ) + } +} + +export default ImageComponent; diff --git a/src/components/app/map/elements/RackFillBar.js b/src/components/app/map/elements/RackFillBar.js new file mode 100644 index 00000000..3a8a1137 --- /dev/null +++ b/src/components/app/map/elements/RackFillBar.js @@ -0,0 +1,67 @@ +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 ( + + + + + + ); +}; + +RackFillBar.propTypes = { + positionX: PropTypes.number.isRequired, + positionY: PropTypes.number.isRequired, + type: PropTypes.string.isRequired, + fillFraction: PropTypes.number.isRequired, +}; + +export default RackFillBar; diff --git a/src/components/app/map/elements/RoomTile.js b/src/components/app/map/elements/RoomTile.js new file mode 100644 index 00000000..11948a7a --- /dev/null +++ b/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}) => ( + +); + +RoomTile.propTypes = { + tile: Shapes.Tile, +}; + +export default RoomTile; diff --git a/src/components/app/map/elements/TileObject.js b/src/components/app/map/elements/TileObject.js new file mode 100644 index 00000000..73bfddba --- /dev/null +++ b/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}) => ( + +); + +TileObject.propTypes = { + positionX: PropTypes.number.isRequired, + positionY: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, +}; + +export default TileObject; diff --git a/src/components/app/map/elements/TilePlusIcon.js b/src/components/app/map/elements/TilePlusIcon.js new file mode 100644 index 00000000..b96bf0f5 --- /dev/null +++ b/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 ( + + {linePoints.map((points, index) => ( + + ))} + + ) +}; + +TilePlusIcon.propTypes = { + pixelX: PropTypes.number, + pixelY: PropTypes.number, + mapScale: PropTypes.number, +}; + +export default TilePlusIcon; diff --git a/src/components/app/map/elements/WallSegment.js b/src/components/app/map/elements/WallSegment.js new file mode 100644 index 00000000..14efd3fc --- /dev/null +++ b/src/components/app/map/elements/WallSegment.js @@ -0,0 +1,39 @@ +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 ( + + ) +}; + +WallSegment.propTypes = { + wallSegment: Shapes.WallSegment, +}; + +export default WallSegment; diff --git a/src/components/app/map/groups/DatacenterGroup.js b/src/components/app/map/groups/DatacenterGroup.js new file mode 100644 index 00000000..1c978360 --- /dev/null +++ b/src/components/app/map/groups/DatacenterGroup.js @@ -0,0 +1,42 @@ +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 DatacenterGroup = ({datacenter, interactionLevel}) => { + if (!datacenter) { + return ; + } + + if (interactionLevel.mode === "BUILDING") { + return ( + + {datacenter.roomIds.map(roomId => ( + + ))} + + ); + } + + return ( + + {datacenter.roomIds + .filter(roomId => roomId !== interactionLevel.roomId) + .map(roomId => ) + } + {interactionLevel.mode === "ROOM" ? : null} + {datacenter.roomIds + .filter(roomId => roomId === interactionLevel.roomId) + .map(roomId => ) + } + + ); +}; + +DatacenterGroup.propTypes = { + datacenter: Shapes.Datacenter, + interactionLevel: Shapes.InteractionLevel, +}; + +export default DatacenterGroup; diff --git a/src/components/app/map/groups/GridGroup.js b/src/components/app/map/groups/GridGroup.js new file mode 100644 index 00000000..b3c6e1d5 --- /dev/null +++ b/src/components/app/map/groups/GridGroup.js @@ -0,0 +1,30 @@ +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 = () => ( + + {HORIZONTAL_POINT_PAIRS.concat(VERTICAL_POINT_PAIRS).map((points, index) => ( + + ))} + +); + +export default GridGroup; diff --git a/src/components/app/map/groups/RackGroup.js b/src/components/app/map/groups/RackGroup.js new file mode 100644 index 00000000..233d0c20 --- /dev/null +++ b/src/components/app/map/groups/RackGroup.js @@ -0,0 +1,34 @@ +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 {convertLoadToSimulationColor} from "../../../../util/simulation-load"; +import TileObject from "../elements/TileObject"; + +const RackGroup = ({tile, inSimulation, rackLoad}) => { + let color = RACK_BACKGROUND_COLOR; + if (inSimulation && rackLoad) { + color = convertLoadToSimulationColor(rackLoad); + } + + return ( + + + {inSimulation ? + undefined : + + + + + } + + ); +}; + +RackGroup.propTypes = { + tile: Shapes.Tile, +}; + +export default RackGroup; diff --git a/src/components/app/map/groups/RoomGroup.js b/src/components/app/map/groups/RoomGroup.js new file mode 100644 index 00000000..18a6bd84 --- /dev/null +++ b/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 ( + + {room.tileIds.map(tileId => ( + + ))} + + ); + } + + return ( + + {(() => { + if ((interactionLevel.mode === "RACK" || interactionLevel.mode === "MACHINE") + && interactionLevel.roomId === room.id) { + return [ + room.tileIds + .filter(tileId => tileId !== interactionLevel.tileId) + .map(tileId => ), + , + room.tileIds + .filter(tileId => tileId === interactionLevel.tileId) + .map(tileId => ) + ]; + } else { + return room.tileIds.map(tileId => ( + + )); + } + })()} + + + ); +}; + +RoomGroup.propTypes = { + room: Shapes.Room, +}; + +export default RoomGroup; diff --git a/src/components/app/map/groups/TileGroup.js b/src/components/app/map/groups/TileGroup.js new file mode 100644 index 00000000..c41e78a4 --- /dev/null +++ b/src/components/app/map/groups/TileGroup.js @@ -0,0 +1,42 @@ +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 {convertLoadToSimulationColor} from "../../../../util/simulation-load"; +import RoomTile from "../elements/RoomTile"; + +const TileGroup = ({tile, newTile, inSimulation, roomLoad, onClick}) => { + let tileObject; + switch (tile.objectType) { + case "RACK": + tileObject = ; + break; + default: + tileObject = null; + } + + let color = ROOM_DEFAULT_COLOR; + if (newTile) { + color = ROOM_IN_CONSTRUCTION_COLOR; + } else if (inSimulation && roomLoad) { + color = convertLoadToSimulationColor(roomLoad); + } + + return ( + onClick(tile)} + > + + {tileObject} + + ); +}; + +TileGroup.propTypes = { + tile: Shapes.Tile, + newTile: PropTypes.bool, +}; + +export default TileGroup; diff --git a/src/components/app/map/groups/WallGroup.js b/src/components/app/map/groups/WallGroup.js new file mode 100644 index 00000000..6de22523 --- /dev/null +++ b/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 ( + + {deriveWallLocations(tiles).map((wallSegment, index) => ( + + ))} + + ); +}; + +WallGroup.propTypes = { + tiles: PropTypes.arrayOf(Shapes.Tile).isRequired, +}; + +export default WallGroup; diff --git a/src/components/app/map/layers/HoverLayerComponent.js b/src/components/app/map/layers/HoverLayerComponent.js new file mode 100644 index 00000000..aa2e8313 --- /dev/null +++ b/src/components/app/map/layers/HoverLayerComponent.js @@ -0,0 +1,63 @@ +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 ; + } + + 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 ( + + 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 + } + + ); + } +} + +export default HoverLayerComponent; diff --git a/src/components/app/map/layers/MapLayerComponent.js b/src/components/app/map/layers/MapLayerComponent.js new file mode 100644 index 00000000..c969249c --- /dev/null +++ b/src/components/app/map/layers/MapLayerComponent.js @@ -0,0 +1,17 @@ +import React from 'react'; +import {Group, Layer} from "react-konva"; +import DatacenterContainer from "../../../../containers/app/map/DatacenterContainer"; +import Backdrop from "../elements/Backdrop"; +import GridGroup from "../groups/GridGroup"; + +const MapLayerComponent = ({mapPosition, mapScale}) => ( + + + + + + + +); + +export default MapLayerComponent; diff --git a/src/components/app/map/layers/ObjectHoverLayerComponent.js b/src/components/app/map/layers/ObjectHoverLayerComponent.js new file mode 100644 index 00000000..aa79f8c3 --- /dev/null +++ b/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) => ( + + + +); + +export default ObjectHoverLayerComponent; diff --git a/src/components/app/map/layers/RoomHoverLayerComponent.js b/src/components/app/map/layers/RoomHoverLayerComponent.js new file mode 100644 index 00000000..2133c8d8 --- /dev/null +++ b/src/components/app/map/layers/RoomHoverLayerComponent.js @@ -0,0 +1,8 @@ +import React from 'react'; +import HoverLayerComponent from "./HoverLayerComponent"; + +const RoomHoverLayerComponent = (props) => ( + +); + +export default RoomHoverLayerComponent; diff --git a/src/components/app/sidebars/Sidebar.js b/src/components/app/sidebars/Sidebar.js new file mode 100644 index 00000000..00e3607a --- /dev/null +++ b/src/components/app/sidebars/Sidebar.js @@ -0,0 +1,38 @@ +import classNames from "classnames"; +import React from "react"; +import "./Sidebar.css"; + +class Sidebar extends React.Component { + state = { + collapsed: false + }; + + render() { + const collapseButton = ( +
this.setState({collapsed: !this.state.collapsed})} + > + {(this.state.collapsed && this.props.isRight) || (!this.state.collapsed && !this.props.isRight) ? + : + + } +
+ ); + + if (this.state.collapsed) { + return collapseButton; + } + return ( +
e.stopPropagation()} + > + {this.props.children} + {collapseButton} +
+ ); + } +} + +export default Sidebar; diff --git a/src/components/app/sidebars/Sidebar.sass b/src/components/app/sidebars/Sidebar.sass new file mode 100644 index 00000000..4d0e5f1e --- /dev/null +++ b/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: 350px + + 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/src/components/app/sidebars/elements/LoadBarComponent.js b/src/components/app/sidebars/elements/LoadBarComponent.js new file mode 100644 index 00000000..65f94b3d --- /dev/null +++ b/src/components/app/sidebars/elements/LoadBarComponent.js @@ -0,0 +1,22 @@ +import classNames from "classnames"; +import React from "react"; + +const LoadBarComponent = ({percent, disabled}) => ( +
+ Current load +
+
+ {percent}% +
+
+
+); + +export default LoadBarComponent; diff --git a/src/components/app/sidebars/elements/LoadChartComponent.js b/src/components/app/sidebars/elements/LoadChartComponent.js new file mode 100644 index 00000000..19d58f77 --- /dev/null +++ b/src/components/app/sidebars/elements/LoadChartComponent.js @@ -0,0 +1,40 @@ +import React from "react"; +import {VictoryAxis, VictoryChart, VictoryLine, VictoryScatter} from "victory"; +import {convertSecondsToFormattedTime} from "../../../../util/date-time"; + +const LoadChartComponent = ({data, currentTick}) => ( +
+ Load over time + + convertSecondsToFormattedTime(tick)} + fixLabelOverlap={true} + label="Simulated Time" + /> + + + + + +
+); + +export default LoadChartComponent; diff --git a/src/components/app/sidebars/simulation/ExperimentMetadataComponent.js b/src/components/app/sidebars/simulation/ExperimentMetadataComponent.js new file mode 100644 index 00000000..3649045b --- /dev/null +++ b/src/components/app/sidebars/simulation/ExperimentMetadataComponent.js @@ -0,0 +1,12 @@ +import React from "react"; + +const ExperimentMetadataComponent = ({experimentName, pathName, traceName, schedulerName}) => ( +
+

{experimentName}

+

Path: {pathName}

+

Trace: {traceName}

+

Scheduler: {schedulerName}

+
+); + +export default ExperimentMetadataComponent; diff --git a/src/components/app/sidebars/simulation/LoadMetricComponent.js b/src/components/app/sidebars/simulation/LoadMetricComponent.js new file mode 100644 index 00000000..e72e6b67 --- /dev/null +++ b/src/components/app/sidebars/simulation/LoadMetricComponent.js @@ -0,0 +1,33 @@ +import React from "react"; +import {SIM_HIGH_COLOR, SIM_LOW_COLOR, SIM_MID_HIGH_COLOR, SIM_MID_LOW_COLOR} from "../../../../util/colors"; +import {LOAD_NAME_MAP} from "../../../../util/simulation-load"; + +const LoadMetricComponent = ({loadMetric}) => ( +
+
Colors represent {LOAD_NAME_MAP[loadMetric]}
+
+ + + + +
+
+); + +export default LoadMetricComponent; diff --git a/src/components/app/sidebars/simulation/SimulationSidebarComponent.js b/src/components/app/sidebars/simulation/SimulationSidebarComponent.js new file mode 100644 index 00000000..92651dfc --- /dev/null +++ b/src/components/app/sidebars/simulation/SimulationSidebarComponent.js @@ -0,0 +1,22 @@ +import React from "react"; +import ExperimentMetadataContainer from "../../../../containers/app/sidebars/simulation/ExperimentMetadataContainer"; +import LoadMetricContainer from "../../../../containers/app/sidebars/simulation/LoadMetricContainer"; +import TraceContainer from "../../../../containers/app/sidebars/simulation/TraceContainer"; +import Sidebar from "../Sidebar"; +import "./SimulationSidebarComponent.css"; + +const SimulationSidebarComponent = () => { + return ( + +
+ + +
+ +
+
+
+ ); +}; + +export default SimulationSidebarComponent; diff --git a/src/components/app/sidebars/simulation/SimulationSidebarComponent.sass b/src/components/app/sidebars/simulation/SimulationSidebarComponent.sass new file mode 100644 index 00000000..82af97fa --- /dev/null +++ b/src/components/app/sidebars/simulation/SimulationSidebarComponent.sass @@ -0,0 +1,8 @@ +.simulation-sidebar-container + display: flex + height: 100% + max-height: 100% + +.trace-container + flex: 1 + overflow-y: scroll diff --git a/src/components/app/sidebars/simulation/TaskComponent.js b/src/components/app/sidebars/simulation/TaskComponent.js new file mode 100644 index 00000000..f7f65817 --- /dev/null +++ b/src/components/app/sidebars/simulation/TaskComponent.js @@ -0,0 +1,42 @@ +import approx from "approximate-number"; +import React from "react"; +import {convertSecondsToFormattedTime} from "../../../../util/date-time"; + +const TaskComponent = ({task, flopsLeft}) => { + let stateInfo; + + if (flopsLeft === task.totalFlopCount) { + stateInfo = ( +
+ + Waiting +
+ ); + } else if (flopsLeft > 0) { + stateInfo = ( +
+ + Running ({approx(task.totalFlopCount - flopsLeft)} / {approx(task.totalFlopCount)} FLOPS) +
+ ); + } else { + stateInfo = ( +
+ + Completed +
+ ); + } + + return ( +
  • +
    +
    {approx(task.totalFlopCount)} FLOPS
    + Starts at {convertSecondsToFormattedTime(task.startTick)} +
    + {stateInfo} +
  • + ); +}; + +export default TaskComponent; diff --git a/src/components/app/sidebars/simulation/TraceComponent.js b/src/components/app/sidebars/simulation/TraceComponent.js new file mode 100644 index 00000000..b43a8cea --- /dev/null +++ b/src/components/app/sidebars/simulation/TraceComponent.js @@ -0,0 +1,20 @@ +import React from "react"; +import TaskContainer from "../../../../containers/app/sidebars/simulation/TaskContainer"; + +const TraceComponent = ({jobs}) => ( +
    +

    Trace

    + {jobs.map(job => ( +
    +

    Job: {job.name}

    +
      + {job.taskIds.map(taskId => ( + + ))} +
    +
    + ))} +
    +); + +export default TraceComponent; diff --git a/src/components/app/sidebars/topology/NameComponent.js b/src/components/app/sidebars/topology/NameComponent.js new file mode 100644 index 00000000..d663f4ae --- /dev/null +++ b/src/components/app/sidebars/topology/NameComponent.js @@ -0,0 +1,13 @@ +import React from "react"; +import FontAwesome from "react-fontawesome"; + +const NameComponent = ({name, onEdit}) => ( +

    + {name} + +

    +); + +export default NameComponent; diff --git a/src/components/app/sidebars/topology/TopologySidebarComponent.js b/src/components/app/sidebars/topology/TopologySidebarComponent.js new file mode 100644 index 00000000..ff4260a9 --- /dev/null +++ b/src/components/app/sidebars/topology/TopologySidebarComponent.js @@ -0,0 +1,35 @@ +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 = ; + break; + case "ROOM": + sidebarContent = ; + break; + case "RACK": + sidebarContent = ; + break; + case "MACHINE": + sidebarContent = ; + break; + default: + sidebarContent = "Missing Content"; + } + + return ( + + {sidebarContent} + + ); +}; + +export default TopologySidebarComponent; diff --git a/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js b/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js new file mode 100644 index 00000000..2bf81a48 --- /dev/null +++ b/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js @@ -0,0 +1,19 @@ +import React from "react"; +import NewRoomConstructionContainer from "../../../../../containers/app/sidebars/topology/building/NewRoomConstructionContainer"; + +const BuildingSidebarComponent = ({inSimulation}) => { + return ( +
    +

    Building

    + {inSimulation ? +
    + + Click on individual rooms to see their stats! +
    : + + } +
    + ); +}; + +export default BuildingSidebarComponent; diff --git a/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js b/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js new file mode 100644 index 00000000..d89b0ac0 --- /dev/null +++ b/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js @@ -0,0 +1,27 @@ +import React from "react"; + +const NewRoomConstructionComponent = ({onStart, onFinish, onCancel, currentRoomInConstruction}) => { + if (currentRoomInConstruction === -1) { + return ( +
    + + Construct a new room +
    + ); + } + return ( +
    +
    + + Finalize new room +
    +
    + + Cancel construction +
    +
    + ); + +}; + +export default NewRoomConstructionComponent; diff --git a/src/components/app/sidebars/topology/machine/BackToRackComponent.js b/src/components/app/sidebars/topology/machine/BackToRackComponent.js new file mode 100644 index 00000000..19e33904 --- /dev/null +++ b/src/components/app/sidebars/topology/machine/BackToRackComponent.js @@ -0,0 +1,10 @@ +import React from "react"; + +const BackToRackComponent = ({onClick}) => ( +
    + + Back to rack +
    +); + +export default BackToRackComponent; diff --git a/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js b/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js new file mode 100644 index 00000000..7ba08352 --- /dev/null +++ b/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js @@ -0,0 +1,10 @@ +import React from "react"; + +const DeleteMachineComponent = ({onClick}) => ( +
    + + Delete this machine +
    +); + +export default DeleteMachineComponent; diff --git a/src/components/app/sidebars/topology/machine/MachineNameComponent.js b/src/components/app/sidebars/topology/machine/MachineNameComponent.js new file mode 100644 index 00000000..321e350d --- /dev/null +++ b/src/components/app/sidebars/topology/machine/MachineNameComponent.js @@ -0,0 +1,7 @@ +import React from "react"; + +const MachineNameComponent = ({position}) => ( +

    Machine at slot {position}

    +); + +export default MachineNameComponent; diff --git a/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js b/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js new file mode 100644 index 00000000..cf4db80e --- /dev/null +++ b/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js @@ -0,0 +1,26 @@ +import React from "react"; +import LoadBarContainer from "../../../../../containers/app/sidebars/elements/LoadBarContainer"; +import LoadChartContainer from "../../../../../containers/app/sidebars/elements/LoadChartContainer"; +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 = ({inSimulation, machineId}) => { + return ( +
    + + + {inSimulation ? +
    + + +
    : + + } + +
    + ); +}; + +export default MachineSidebarComponent; diff --git a/src/components/app/sidebars/topology/machine/UnitAddComponent.js b/src/components/app/sidebars/topology/machine/UnitAddComponent.js new file mode 100644 index 00000000..f16700df --- /dev/null +++ b/src/components/app/sidebars/topology/machine/UnitAddComponent.js @@ -0,0 +1,38 @@ +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 ( +
    +
    + + +
    +
    + ); + } +} + +export default UnitAddComponent; diff --git a/src/components/app/sidebars/topology/machine/UnitComponent.js b/src/components/app/sidebars/topology/machine/UnitComponent.js new file mode 100644 index 00000000..c734f508 --- /dev/null +++ b/src/components/app/sidebars/topology/machine/UnitComponent.js @@ -0,0 +1,16 @@ +import React from "react"; + +const UnitComponent = ({unit, onDelete, inSimulation}) => ( +
  • + {unit.manufacturer + " " + unit.family + " " + unit.model + " " + unit.generation} + {inSimulation ? + undefined : + + + Delete + + } +
  • +); + +export default UnitComponent; diff --git a/src/components/app/sidebars/topology/machine/UnitListComponent.js b/src/components/app/sidebars/topology/machine/UnitListComponent.js new file mode 100644 index 00000000..683f6023 --- /dev/null +++ b/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, inSimulation}) => ( +
      + {unitIds.length !== 0 ? + unitIds.map((unitId, index) => ( + + )) : +
      + {inSimulation ? + No units of this type in this machine : + No units... Add some with the menu above! + } +
      + } +
    +); + +export default UnitListComponent; diff --git a/src/components/app/sidebars/topology/machine/UnitTabsComponent.js b/src/components/app/sidebars/topology/machine/UnitTabsComponent.js new file mode 100644 index 00000000..2113d6d8 --- /dev/null +++ b/src/components/app/sidebars/topology/machine/UnitTabsComponent.js @@ -0,0 +1,54 @@ +import React from "react"; +import UnitAddContainer from "../../../../../containers/app/sidebars/topology/machine/UnitAddContainer"; +import UnitListContainer from "../../../../../containers/app/sidebars/topology/machine/UnitListContainer"; + +const UnitTabsComponent = ({inSimulation}) => ( +
    + +
    +
    + {inSimulation ? + undefined : + + } + +
    +
    + {inSimulation ? + undefined : + + } + +
    +
    + {inSimulation ? + undefined : + + } + +
    +
    + {inSimulation ? + undefined : + + } + +
    +
    +
    +); + +export default UnitTabsComponent; diff --git a/src/components/app/sidebars/topology/rack/BackToRoomComponent.js b/src/components/app/sidebars/topology/rack/BackToRoomComponent.js new file mode 100644 index 00000000..267001c6 --- /dev/null +++ b/src/components/app/sidebars/topology/rack/BackToRoomComponent.js @@ -0,0 +1,10 @@ +import React from "react"; + +const BackToRoomComponent = ({onClick}) => ( +
    + + Back to room +
    +); + +export default BackToRoomComponent; diff --git a/src/components/app/sidebars/topology/rack/DeleteRackComponent.js b/src/components/app/sidebars/topology/rack/DeleteRackComponent.js new file mode 100644 index 00000000..b268bd72 --- /dev/null +++ b/src/components/app/sidebars/topology/rack/DeleteRackComponent.js @@ -0,0 +1,10 @@ +import React from "react"; + +const DeleteRackComponent = ({onClick}) => ( +
    + + Delete this rack +
    +); + +export default DeleteRackComponent; diff --git a/src/components/app/sidebars/topology/rack/EmptySlotComponent.js b/src/components/app/sidebars/topology/rack/EmptySlotComponent.js new file mode 100644 index 00000000..08665072 --- /dev/null +++ b/src/components/app/sidebars/topology/rack/EmptySlotComponent.js @@ -0,0 +1,20 @@ +import React from "react"; + +const EmptySlotComponent = ({position, onAdd, inSimulation}) => ( +
  • + + {position} + + {inSimulation ? + + Empty Slot + : + + } +
  • +); + +export default EmptySlotComponent; diff --git a/src/components/app/sidebars/topology/rack/MachineComponent.js b/src/components/app/sidebars/topology/rack/MachineComponent.js new file mode 100644 index 00000000..545bb916 --- /dev/null +++ b/src/components/app/sidebars/topology/rack/MachineComponent.js @@ -0,0 +1,65 @@ +import React from "react"; +import Shapes from "../../../../../shapes"; +import {convertLoadToSimulationColor} from "../../../../../util/simulation-load"; + +const UnitIcon = ({id, type}) => ( +
    + {"Machine +
    +); + +const MachineComponent = ({position, machine, inSimulation, machineLoad, onClick}) => { + let color = "white"; + if (inSimulation && machineLoad) { + color = convertLoadToSimulationColor(machineLoad); + } + const hasNoUnits = machine.cpuIds.length + machine.gpuIds.length + machine.memoryIds.length + + machine.storageIds.length === 0; + + return ( +
  • + + {position} + +
    + {machine.cpuIds.length > 0 ? + : + undefined + } + {machine.gpuIds.length > 0 ? + : + undefined + } + {machine.memoryIds.length > 0 ? + : + undefined + } + {machine.storageIds.length > 0 ? + : + undefined + } + {hasNoUnits ? + + Machine with no units + : + undefined + } +
    +
  • + ); +}; + +MachineComponent.propTypes = { + machine: Shapes.Machine +}; + +export default MachineComponent; diff --git a/src/components/app/sidebars/topology/rack/MachineListComponent.js b/src/components/app/sidebars/topology/rack/MachineListComponent.js new file mode 100644 index 00000000..fcb90d66 --- /dev/null +++ b/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.css"; + +const MachineListComponent = ({machineIds}) => { + return ( +
      + {machineIds.map((machineId, index) => { + if (machineId === null) { + return ; + } else { + return ; + } + })} +
    + ); +}; + +export default MachineListComponent; diff --git a/src/components/app/sidebars/topology/rack/MachineListComponent.sass b/src/components/app/sidebars/topology/rack/MachineListComponent.sass new file mode 100644 index 00000000..bbcfe696 --- /dev/null +++ b/src/components/app/sidebars/topology/rack/MachineListComponent.sass @@ -0,0 +1,2 @@ +.machine-list li + min-height: 64px diff --git a/src/components/app/sidebars/topology/rack/RackNameComponent.js b/src/components/app/sidebars/topology/rack/RackNameComponent.js new file mode 100644 index 00000000..ee8d194b --- /dev/null +++ b/src/components/app/sidebars/topology/rack/RackNameComponent.js @@ -0,0 +1,8 @@ +import React from "react"; +import NameComponent from "../NameComponent"; + +const RackNameComponent = ({rackName, onEdit}) => ( + +); + +export default RackNameComponent; diff --git a/src/components/app/sidebars/topology/rack/RackSidebarComponent.js b/src/components/app/sidebars/topology/rack/RackSidebarComponent.js new file mode 100644 index 00000000..f563a52f --- /dev/null +++ b/src/components/app/sidebars/topology/rack/RackSidebarComponent.js @@ -0,0 +1,33 @@ +import React from "react"; +import LoadBarContainer from "../../../../../containers/app/sidebars/elements/LoadBarContainer"; +import LoadChartContainer from "../../../../../containers/app/sidebars/elements/LoadChartContainer"; +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.css"; + +const RackSidebarComponent = ({inSimulation, rackId}) => { + return ( +
    +
    + + + {inSimulation ? +
    + + +
    : +
    + +
    + } +
    +
    + +
    +
    + ); +}; + +export default RackSidebarComponent; diff --git a/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass b/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass new file mode 100644 index 00000000..822804bc --- /dev/null +++ b/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/src/components/app/sidebars/topology/room/BackToBuildingComponent.js b/src/components/app/sidebars/topology/room/BackToBuildingComponent.js new file mode 100644 index 00000000..81384ba5 --- /dev/null +++ b/src/components/app/sidebars/topology/room/BackToBuildingComponent.js @@ -0,0 +1,10 @@ +import React from "react"; + +const BackToBuildingComponent = ({onClick}) => ( +
    + + Back to building +
    +); + +export default BackToBuildingComponent; diff --git a/src/components/app/sidebars/topology/room/DeleteRoomComponent.js b/src/components/app/sidebars/topology/room/DeleteRoomComponent.js new file mode 100644 index 00000000..3f41eac0 --- /dev/null +++ b/src/components/app/sidebars/topology/room/DeleteRoomComponent.js @@ -0,0 +1,10 @@ +import React from "react"; + +const DeleteRoomComponent = ({onClick}) => ( +
    + + Delete this room +
    +); + +export default DeleteRoomComponent; diff --git a/src/components/app/sidebars/topology/room/RackConstructionComponent.js b/src/components/app/sidebars/topology/room/RackConstructionComponent.js new file mode 100644 index 00000000..9bfe28ce --- /dev/null +++ b/src/components/app/sidebars/topology/room/RackConstructionComponent.js @@ -0,0 +1,21 @@ +import React from "react"; + +const RackConstructionComponent = ({inRackConstructionMode, onStart, onStop}) => { + if (inRackConstructionMode) { + return ( +
    + + Stop rack construction +
    + ); + } + + return ( +
    + + Start rack construction +
    + ); +}; + +export default RackConstructionComponent; diff --git a/src/components/app/sidebars/topology/room/RoomNameComponent.js b/src/components/app/sidebars/topology/room/RoomNameComponent.js new file mode 100644 index 00000000..4d3e41cc --- /dev/null +++ b/src/components/app/sidebars/topology/room/RoomNameComponent.js @@ -0,0 +1,8 @@ +import React from "react"; +import NameComponent from "../NameComponent"; + +const RoomNameComponent = ({roomName, onEdit}) => ( + +); + +export default RoomNameComponent; diff --git a/src/components/app/sidebars/topology/room/RoomSidebarComponent.js b/src/components/app/sidebars/topology/room/RoomSidebarComponent.js new file mode 100644 index 00000000..53857408 --- /dev/null +++ b/src/components/app/sidebars/topology/room/RoomSidebarComponent.js @@ -0,0 +1,35 @@ +import React from "react"; +import LoadBarContainer from "../../../../../containers/app/sidebars/elements/LoadBarContainer"; +import LoadChartContainer from "../../../../../containers/app/sidebars/elements/LoadChartContainer"; +import BackToBuildingContainer from "../../../../../containers/app/sidebars/topology/room/BackToBuildingContainer"; +import DeleteRoomContainer from "../../../../../containers/app/sidebars/topology/room/DeleteRoomContainer"; +import RackConstructionContainer from "../../../../../containers/app/sidebars/topology/room/RackConstructionContainer"; +import RoomNameContainer from "../../../../../containers/app/sidebars/topology/room/RoomNameContainer"; +import RoomTypeContainer from "../../../../../containers/app/sidebars/topology/room/RoomTypeContainer"; + +const RoomSidebarComponent = ({roomId, roomType, inSimulation}) => { + let allowedObjects; + if (!inSimulation && roomType === "SERVER") { + allowedObjects = ; + } + + return ( +
    + + + + {inSimulation ? +
    + + +
    : +
    + {allowedObjects} + +
    + } +
    + ); +}; + +export default RoomSidebarComponent; diff --git a/src/components/app/sidebars/topology/room/RoomTypeComponent.js b/src/components/app/sidebars/topology/room/RoomTypeComponent.js new file mode 100644 index 00000000..d42eefb6 --- /dev/null +++ b/src/components/app/sidebars/topology/room/RoomTypeComponent.js @@ -0,0 +1,10 @@ +import React from "react"; +import {ROOM_TYPE_TO_NAME_MAP} from "../../../../../util/room-types"; + +const RoomTypeComponent = ({roomType}) => ( +

    + {ROOM_TYPE_TO_NAME_MAP[roomType]} +

    +); + +export default RoomTypeComponent; diff --git a/src/components/app/timeline/PlayButtonComponent.js b/src/components/app/timeline/PlayButtonComponent.js new file mode 100644 index 00000000..6ec70cc3 --- /dev/null +++ b/src/components/app/timeline/PlayButtonComponent.js @@ -0,0 +1,12 @@ +import React from "react"; + +const PlayButtonComponent = ({isPlaying, onPlay, onPause}) => ( +
    isPlaying ? onPause() : onPlay()}> + {isPlaying ? + : + + } +
    +); + +export default PlayButtonComponent; diff --git a/src/components/app/timeline/Timeline.sass b/src/components/app/timeline/Timeline.sass new file mode 100644 index 00000000..5003e80d --- /dev/null +++ b/src/components/app/timeline/Timeline.sass @@ -0,0 +1,112 @@ +@import ../../../style-globals/_variables.sass +@import ../../../style-globals/_mixins.sass + +$container-size: 500px +$play-btn-size: 40px +$border-width: 1px +$timeline-border: $border-width solid $gray-semi-dark + +.timeline-bar + display: block + position: absolute + left: 0 + bottom: 20px + width: 100% + text-align: center + z-index: 2000 + + pointer-events: none + +.timeline-container + display: inline-block + margin: 0 auto + text-align: left + + width: $container-size + +.timeline-labels + display: block + height: 25px + line-height: 25px + + div + display: inline-block + + .start-time-label + margin-left: $play-btn-size - $border-width + padding-left: 4px + + .end-time-label + padding-right: 4px + float: right + +.timeline-controls + display: flex + border: $timeline-border + overflow: hidden + + pointer-events: all + + +border-radius($standard-border-radius) + + .play-btn + width: $play-btn-size + height: $play-btn-size + $border-width + line-height: $play-btn-size + $border-width + text-align: center + float: left + margin-top: -$border-width + + font-size: 16pt + background: #333 + color: #eee + + +transition(background, $transition-length) + +user-select + +clickable + + .play-btn:hover + background: #656565 + + .play-btn:active + background: #000 + + .timeline + position: relative + flex: 1 + height: $play-btn-size + line-height: $play-btn-size + float: right + + background: $blue-light + + z-index: 500 + + div + +transition(all, $transition-length) + + .time-marker + position: absolute + top: 0 + left: 0 + + width: 6px + height: 100% + + background: $blue-very-dark + + +border-radius(2px) + + z-index: 503 + + .section-marker + position: absolute + top: 0 + left: 0 + + width: 3px + height: 100% + + background: #222222 + + z-index: 504 diff --git a/src/components/app/timeline/TimelineComponent.js b/src/components/app/timeline/TimelineComponent.js new file mode 100644 index 00000000..950a25bd --- /dev/null +++ b/src/components/app/timeline/TimelineComponent.js @@ -0,0 +1,37 @@ +import React from "react"; +import TimelineControlsContainer from "../../../containers/app/timeline/TimelineControlsContainer"; +import TimelineLabelsContainer from "../../../containers/app/timeline/TimelineLabelsContainer"; +import "./Timeline.css"; + +class TimelineComponent extends React.Component { + componentDidMount() { + this.interval = setInterval(() => { + if (!this.props.isPlaying) { + return; + } + + if (this.props.currentTick < this.props.lastSimulatedTick) { + this.props.incrementTick(); + } else { + this.props.pauseSimulation(); + } + }, 1000); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + render() { + return ( +
    +
    + + +
    +
    + ); + } +} + +export default TimelineComponent; diff --git a/src/components/app/timeline/TimelineControlsComponent.js b/src/components/app/timeline/TimelineControlsComponent.js new file mode 100644 index 00000000..72fc4a60 --- /dev/null +++ b/src/components/app/timeline/TimelineControlsComponent.js @@ -0,0 +1,39 @@ +import React from "react"; +import PlayButtonContainer from "../../../containers/app/timeline/PlayButtonContainer"; +import {convertTickToPercentage} from "../../../util/timeline"; + +class TimelineControlsComponent extends React.Component { + onTimelineClick(e) { + const percentage = e.nativeEvent.offsetX / this.timeline.clientWidth; + const tick = Math.floor(percentage * (this.props.lastSimulatedTick + 1)); + this.props.goToTick(tick); + } + + render() { + return ( +
    + +
    this.timeline = timeline} + onClick={this.onTimelineClick.bind(this)} + > +
    + {this.props.sectionTicks.map(sectionTick => ( +
    + ))} +
    +
    + ); + } +} + +export default TimelineControlsComponent; diff --git a/src/components/app/timeline/TimelineLabelsComponent.js b/src/components/app/timeline/TimelineLabelsComponent.js new file mode 100644 index 00000000..e795691f --- /dev/null +++ b/src/components/app/timeline/TimelineLabelsComponent.js @@ -0,0 +1,11 @@ +import React from "react"; +import {convertSecondsToFormattedTime} from "../../../util/date-time"; + +const TimelineLabelsComponent = ({currentTick, lastSimulatedTick}) => ( +
    +
    {convertSecondsToFormattedTime(currentTick)}
    +
    {convertSecondsToFormattedTime(lastSimulatedTick)}
    +
    +); + +export default TimelineLabelsComponent; -- cgit v1.2.3