From 90fae26aa4bd0e0eb3272ff6e6524060e9004fbb Mon Sep 17 00:00:00 2001 From: Georgios Andreadis Date: Mon, 29 Jun 2020 15:47:09 +0200 Subject: Prepare frontend repository for monorepo This change prepares the frontend Git repository for the monorepo residing at https://github.com/atlarge-research.com/opendc. To accomodate for this, we move all files into a frontend subdirectory. --- frontend/src/components/app/map/LoadingScreen.js | 11 ++ frontend/src/components/app/map/MapConstants.js | 29 +++++ .../src/components/app/map/MapStageComponent.js | 126 ++++++++++++++++++++ .../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 | 24 ++++ .../src/components/app/map/elements/Backdrop.js | 16 +++ .../src/components/app/map/elements/GrayLayer.js | 17 +++ .../src/components/app/map/elements/HoverTile.js | 30 +++++ .../components/app/map/elements/ImageComponent.js | 48 ++++++++ .../src/components/app/map/elements/RackFillBar.js | 89 ++++++++++++++ .../src/components/app/map/elements/RoomTile.js | 20 ++++ .../src/components/app/map/elements/TileObject.js | 29 +++++ .../components/app/map/elements/TilePlusIcon.js | 52 ++++++++ .../src/components/app/map/elements/WallSegment.js | 39 ++++++ .../components/app/map/groups/DatacenterGroup.js | 40 +++++++ .../src/components/app/map/groups/GridGroup.js | 41 +++++++ .../src/components/app/map/groups/RackGroup.js | 43 +++++++ .../src/components/app/map/groups/RoomGroup.js | 56 +++++++++ .../src/components/app/map/groups/TileGroup.js | 43 +++++++ .../src/components/app/map/groups/WallGroup.js | 22 ++++ .../app/map/layers/HoverLayerComponent.js | 85 +++++++++++++ .../components/app/map/layers/MapLayerComponent.js | 22 ++++ .../app/map/layers/ObjectHoverLayerComponent.js | 11 ++ .../app/map/layers/RoomHoverLayerComponent.js | 6 + frontend/src/components/app/sidebars/Sidebar.js | 50 ++++++++ frontend/src/components/app/sidebars/Sidebar.sass | 50 ++++++++ .../app/sidebars/elements/LoadBarComponent.js | 22 ++++ .../app/sidebars/elements/LoadChartComponent.js | 90 ++++++++++++++ .../simulation/ExperimentMetadataComponent.js | 23 ++++ .../app/sidebars/simulation/LoadMetricComponent.js | 40 +++++++ .../simulation/SimulationSidebarComponent.js | 22 ++++ .../simulation/SimulationSidebarComponent.sass | 8 ++ .../app/sidebars/simulation/TaskComponent.js | 58 +++++++++ .../app/sidebars/simulation/TraceComponent.js | 20 ++++ .../app/sidebars/topology/NameComponent.js | 13 ++ .../sidebars/topology/TopologySidebarComponent.js | 31 +++++ .../topology/building/BuildingSidebarComponent.js | 20 ++++ .../building/NewRoomConstructionComponent.js | 31 +++++ .../topology/machine/BackToRackComponent.js | 10 ++ .../topology/machine/DeleteMachineComponent.js | 10 ++ .../topology/machine/MachineNameComponent.js | 7 ++ .../topology/machine/MachineSidebarComponent.js | 27 +++++ .../sidebars/topology/machine/UnitAddComponent.js | 46 +++++++ .../app/sidebars/topology/machine/UnitComponent.js | 78 ++++++++++++ .../sidebars/topology/machine/UnitListComponent.js | 29 +++++ .../sidebars/topology/machine/UnitTabsComponent.js | 65 ++++++++++ .../sidebars/topology/rack/BackToRoomComponent.js | 10 ++ .../sidebars/topology/rack/DeleteRackComponent.js | 10 ++ .../sidebars/topology/rack/EmptySlotComponent.js | 19 +++ .../app/sidebars/topology/rack/MachineComponent.js | 78 ++++++++++++ .../sidebars/topology/rack/MachineListComponent.js | 26 ++++ .../topology/rack/MachineListComponent.sass | 2 + .../sidebars/topology/rack/RackNameComponent.js | 8 ++ .../sidebars/topology/rack/RackSidebarComponent.js | 34 ++++++ .../topology/rack/RackSidebarComponent.sass | 11 ++ .../topology/room/BackToBuildingComponent.js | 10 ++ .../sidebars/topology/room/DeleteRoomComponent.js | 10 ++ .../sidebars/topology/room/EditRoomComponent.js | 27 +++++ .../topology/room/RackConstructionComponent.js | 32 +++++ .../sidebars/topology/room/RoomNameComponent.js | 8 ++ .../sidebars/topology/room/RoomSidebarComponent.js | 38 ++++++ .../sidebars/topology/room/RoomTypeComponent.js | 8 ++ .../components/app/timeline/PlayButtonComponent.js | 30 +++++ frontend/src/components/app/timeline/Timeline.sass | 116 ++++++++++++++++++ .../components/app/timeline/TimelineComponent.js | 37 ++++++ .../app/timeline/TimelineControlsComponent.js | 49 ++++++++ .../app/timeline/TimelineLabelsComponent.js | 15 +++ .../experiments/ExperimentListComponent.js | 59 +++++++++ .../experiments/ExperimentRowComponent.js | 40 +++++++ .../experiments/NewExperimentButtonComponent.js | 17 +++ frontend/src/components/home/ContactSection.js | 64 ++++++++++ frontend/src/components/home/ContactSection.sass | 15 +++ frontend/src/components/home/ContentSection.js | 19 +++ frontend/src/components/home/ContentSection.sass | 9 ++ frontend/src/components/home/IntroSection.js | 40 +++++++ frontend/src/components/home/JumbotronHeader.js | 20 ++++ frontend/src/components/home/JumbotronHeader.sass | 24 ++++ frontend/src/components/home/ModelingSection.js | 24 ++++ frontend/src/components/home/ScreenshotSection.js | 32 +++++ .../src/components/home/ScreenshotSection.sass | 5 + frontend/src/components/home/SimulationSection.js | 25 ++++ frontend/src/components/home/StakeholderSection.js | 42 +++++++ frontend/src/components/home/TeamSection.js | 56 +++++++++ .../src/components/home/TechnologiesSection.js | 42 +++++++ .../src/components/modals/ConfirmationModal.js | 37 ++++++ frontend/src/components/modals/Modal.js | 132 +++++++++++++++++++++ frontend/src/components/modals/TextInputModal.js | 58 +++++++++ .../NewExperimentModalComponent.js | 104 ++++++++++++++++ frontend/src/components/navigation/AppNavbar.js | 56 +++++++++ frontend/src/components/navigation/HomeNavbar.js | 24 ++++ frontend/src/components/navigation/LogoutButton.js | 16 +++ frontend/src/components/navigation/Navbar.js | 102 ++++++++++++++++ frontend/src/components/navigation/Navbar.sass | 29 +++++ .../src/components/not-found/BlinkingCursor.js | 6 + .../src/components/not-found/BlinkingCursor.sass | 35 ++++++ frontend/src/components/not-found/CodeBlock.js | 34 ++++++ frontend/src/components/not-found/CodeBlock.sass | 3 + .../src/components/not-found/TerminalWindow.js | 29 +++++ .../src/components/not-found/TerminalWindow.sass | 70 +++++++++++ .../src/components/simulations/FilterButton.js | 24 ++++ frontend/src/components/simulations/FilterPanel.js | 13 ++ .../src/components/simulations/FilterPanel.sass | 5 + .../simulations/NewSimulationButtonComponent.js | 17 +++ .../simulations/SimulationActionButtons.js | 37 ++++++ .../components/simulations/SimulationAuthList.js | 43 +++++++ .../components/simulations/SimulationAuthRow.js | 32 +++++ 110 files changed, 3720 insertions(+) create mode 100644 frontend/src/components/app/map/LoadingScreen.js create mode 100644 frontend/src/components/app/map/MapConstants.js create mode 100644 frontend/src/components/app/map/MapStageComponent.js create mode 100644 frontend/src/components/app/map/controls/ExportCanvasComponent.js create mode 100644 frontend/src/components/app/map/controls/ScaleIndicatorComponent.js create mode 100644 frontend/src/components/app/map/controls/ScaleIndicatorComponent.sass create mode 100644 frontend/src/components/app/map/controls/ToolPanelComponent.js create mode 100644 frontend/src/components/app/map/controls/ToolPanelComponent.sass create mode 100644 frontend/src/components/app/map/controls/ZoomControlComponent.js create mode 100644 frontend/src/components/app/map/elements/Backdrop.js create mode 100644 frontend/src/components/app/map/elements/GrayLayer.js create mode 100644 frontend/src/components/app/map/elements/HoverTile.js create mode 100644 frontend/src/components/app/map/elements/ImageComponent.js create mode 100644 frontend/src/components/app/map/elements/RackFillBar.js create mode 100644 frontend/src/components/app/map/elements/RoomTile.js create mode 100644 frontend/src/components/app/map/elements/TileObject.js create mode 100644 frontend/src/components/app/map/elements/TilePlusIcon.js create mode 100644 frontend/src/components/app/map/elements/WallSegment.js create mode 100644 frontend/src/components/app/map/groups/DatacenterGroup.js create mode 100644 frontend/src/components/app/map/groups/GridGroup.js create mode 100644 frontend/src/components/app/map/groups/RackGroup.js create mode 100644 frontend/src/components/app/map/groups/RoomGroup.js create mode 100644 frontend/src/components/app/map/groups/TileGroup.js create mode 100644 frontend/src/components/app/map/groups/WallGroup.js create mode 100644 frontend/src/components/app/map/layers/HoverLayerComponent.js create mode 100644 frontend/src/components/app/map/layers/MapLayerComponent.js create mode 100644 frontend/src/components/app/map/layers/ObjectHoverLayerComponent.js create mode 100644 frontend/src/components/app/map/layers/RoomHoverLayerComponent.js create mode 100644 frontend/src/components/app/sidebars/Sidebar.js create mode 100644 frontend/src/components/app/sidebars/Sidebar.sass create mode 100644 frontend/src/components/app/sidebars/elements/LoadBarComponent.js create mode 100644 frontend/src/components/app/sidebars/elements/LoadChartComponent.js create mode 100644 frontend/src/components/app/sidebars/simulation/ExperimentMetadataComponent.js create mode 100644 frontend/src/components/app/sidebars/simulation/LoadMetricComponent.js create mode 100644 frontend/src/components/app/sidebars/simulation/SimulationSidebarComponent.js create mode 100644 frontend/src/components/app/sidebars/simulation/SimulationSidebarComponent.sass create mode 100644 frontend/src/components/app/sidebars/simulation/TaskComponent.js create mode 100644 frontend/src/components/app/sidebars/simulation/TraceComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/NameComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/TopologySidebarComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/machine/BackToRackComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/machine/MachineNameComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/machine/UnitAddComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/machine/UnitComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/machine/UnitListComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/machine/UnitTabsComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/rack/BackToRoomComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/rack/DeleteRackComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/rack/EmptySlotComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/rack/MachineComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/rack/MachineListComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/rack/MachineListComponent.sass create mode 100644 frontend/src/components/app/sidebars/topology/rack/RackNameComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/rack/RackSidebarComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass create mode 100644 frontend/src/components/app/sidebars/topology/room/BackToBuildingComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/room/DeleteRoomComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/room/EditRoomComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/room/RackConstructionComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/room/RoomNameComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/room/RoomSidebarComponent.js create mode 100644 frontend/src/components/app/sidebars/topology/room/RoomTypeComponent.js create mode 100644 frontend/src/components/app/timeline/PlayButtonComponent.js create mode 100644 frontend/src/components/app/timeline/Timeline.sass create mode 100644 frontend/src/components/app/timeline/TimelineComponent.js create mode 100644 frontend/src/components/app/timeline/TimelineControlsComponent.js create mode 100644 frontend/src/components/app/timeline/TimelineLabelsComponent.js create mode 100644 frontend/src/components/experiments/ExperimentListComponent.js create mode 100644 frontend/src/components/experiments/ExperimentRowComponent.js create mode 100644 frontend/src/components/experiments/NewExperimentButtonComponent.js create mode 100644 frontend/src/components/home/ContactSection.js create mode 100644 frontend/src/components/home/ContactSection.sass create mode 100644 frontend/src/components/home/ContentSection.js create mode 100644 frontend/src/components/home/ContentSection.sass create mode 100644 frontend/src/components/home/IntroSection.js create mode 100644 frontend/src/components/home/JumbotronHeader.js create mode 100644 frontend/src/components/home/JumbotronHeader.sass create mode 100644 frontend/src/components/home/ModelingSection.js create mode 100644 frontend/src/components/home/ScreenshotSection.js create mode 100644 frontend/src/components/home/ScreenshotSection.sass create mode 100644 frontend/src/components/home/SimulationSection.js create mode 100644 frontend/src/components/home/StakeholderSection.js create mode 100644 frontend/src/components/home/TeamSection.js create mode 100644 frontend/src/components/home/TechnologiesSection.js create mode 100644 frontend/src/components/modals/ConfirmationModal.js create mode 100644 frontend/src/components/modals/Modal.js create mode 100644 frontend/src/components/modals/TextInputModal.js create mode 100644 frontend/src/components/modals/custom-components/NewExperimentModalComponent.js create mode 100644 frontend/src/components/navigation/AppNavbar.js create mode 100644 frontend/src/components/navigation/HomeNavbar.js create mode 100644 frontend/src/components/navigation/LogoutButton.js create mode 100644 frontend/src/components/navigation/Navbar.js create mode 100644 frontend/src/components/navigation/Navbar.sass create mode 100644 frontend/src/components/not-found/BlinkingCursor.js create mode 100644 frontend/src/components/not-found/BlinkingCursor.sass create mode 100644 frontend/src/components/not-found/CodeBlock.js create mode 100644 frontend/src/components/not-found/CodeBlock.sass create mode 100644 frontend/src/components/not-found/TerminalWindow.js create mode 100644 frontend/src/components/not-found/TerminalWindow.sass create mode 100644 frontend/src/components/simulations/FilterButton.js create mode 100644 frontend/src/components/simulations/FilterPanel.js create mode 100644 frontend/src/components/simulations/FilterPanel.sass create mode 100644 frontend/src/components/simulations/NewSimulationButtonComponent.js create mode 100644 frontend/src/components/simulations/SimulationActionButtons.js create mode 100644 frontend/src/components/simulations/SimulationAuthList.js create mode 100644 frontend/src/components/simulations/SimulationAuthRow.js (limited to 'frontend/src/components') diff --git a/frontend/src/components/app/map/LoadingScreen.js b/frontend/src/components/app/map/LoadingScreen.js new file mode 100644 index 00000000..9f379e0b --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/MapConstants.js b/frontend/src/components/app/map/MapConstants.js new file mode 100644 index 00000000..32438b5e --- /dev/null +++ b/frontend/src/components/app/map/MapConstants.js @@ -0,0 +1,29 @@ +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/frontend/src/components/app/map/MapStageComponent.js b/frontend/src/components/app/map/MapStageComponent.js new file mode 100644 index 00000000..67b3349c --- /dev/null +++ b/frontend/src/components/app/map/MapStageComponent.js @@ -0,0 +1,126 @@ +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_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); + } + + componentWillMount() { + this.updateDimensions(); + } + + componentDidMount() { + 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( + jQuery(window).width(), + jQuery(window).height() - 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 ( + + { + this.stage = stage; + }} + width={this.props.mapDimensions.width} + height={this.props.mapDimensions.height} + onMouseMove={this.updateMousePosition.bind(this)} + > + + + + + + + + ); + } +} + +export default MapStageComponent; diff --git a/frontend/src/components/app/map/controls/ExportCanvasComponent.js b/frontend/src/components/app/map/controls/ExportCanvasComponent.js new file mode 100644 index 00000000..ee934f21 --- /dev/null +++ b/frontend/src/components/app/map/controls/ExportCanvasComponent.js @@ -0,0 +1,13 @@ +import React from "react"; + +const ExportCanvasComponent = () => ( + +); + +export default ExportCanvasComponent; diff --git a/frontend/src/components/app/map/controls/ScaleIndicatorComponent.js b/frontend/src/components/app/map/controls/ScaleIndicatorComponent.js new file mode 100644 index 00000000..b7b5cc36 --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/controls/ScaleIndicatorComponent.sass b/frontend/src/components/app/map/controls/ScaleIndicatorComponent.sass new file mode 100644 index 00000000..f2d2b55b --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/controls/ToolPanelComponent.js b/frontend/src/components/app/map/controls/ToolPanelComponent.js new file mode 100644 index 00000000..605e9887 --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/controls/ToolPanelComponent.sass b/frontend/src/components/app/map/controls/ToolPanelComponent.sass new file mode 100644 index 00000000..996712b3 --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/controls/ZoomControlComponent.js b/frontend/src/components/app/map/controls/ZoomControlComponent.js new file mode 100644 index 00000000..e1b7491e --- /dev/null +++ b/frontend/src/components/app/map/controls/ZoomControlComponent.js @@ -0,0 +1,24 @@ +import React from "react"; + +const ZoomControlComponent = ({ zoomInOnCenter }) => { + return ( + + + + + ); +}; + +export default ZoomControlComponent; diff --git a/frontend/src/components/app/map/elements/Backdrop.js b/frontend/src/components/app/map/elements/Backdrop.js new file mode 100644 index 00000000..57414463 --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/elements/GrayLayer.js b/frontend/src/components/app/map/elements/GrayLayer.js new file mode 100644 index 00000000..28fadd8a --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/elements/HoverTile.js b/frontend/src/components/app/map/elements/HoverTile.js new file mode 100644 index 00000000..42e6547c --- /dev/null +++ b/frontend/src/components/app/map/elements/HoverTile.js @@ -0,0 +1,30 @@ +import PropTypes from "prop-types"; +import React from "react"; +import { Rect } from "react-konva"; +import { + ROOM_HOVER_INVALID_COLOR, + ROOM_HOVER_VALID_COLOR +} from "../../../../util/colors"; +import { TILE_SIZE_IN_PIXELS } from "../MapConstants"; + +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/frontend/src/components/app/map/elements/ImageComponent.js b/frontend/src/components/app/map/elements/ImageComponent.js new file mode 100644 index 00000000..cf41ddfe --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/elements/RackFillBar.js b/frontend/src/components/app/map/elements/RackFillBar.js new file mode 100644 index 00000000..43701d97 --- /dev/null +++ b/frontend/src/components/app/map/elements/RackFillBar.js @@ -0,0 +1,89 @@ +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/frontend/src/components/app/map/elements/RoomTile.js b/frontend/src/components/app/map/elements/RoomTile.js new file mode 100644 index 00000000..71c3bf15 --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/elements/TileObject.js b/frontend/src/components/app/map/elements/TileObject.js new file mode 100644 index 00000000..c1b631db --- /dev/null +++ b/frontend/src/components/app/map/elements/TileObject.js @@ -0,0 +1,29 @@ +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/frontend/src/components/app/map/elements/TilePlusIcon.js b/frontend/src/components/app/map/elements/TilePlusIcon.js new file mode 100644 index 00000000..06377152 --- /dev/null +++ b/frontend/src/components/app/map/elements/TilePlusIcon.js @@ -0,0 +1,52 @@ +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/frontend/src/components/app/map/elements/WallSegment.js b/frontend/src/components/app/map/elements/WallSegment.js new file mode 100644 index 00000000..c5011656 --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/groups/DatacenterGroup.js b/frontend/src/components/app/map/groups/DatacenterGroup.js new file mode 100644 index 00000000..51e32db6 --- /dev/null +++ b/frontend/src/components/app/map/groups/DatacenterGroup.js @@ -0,0 +1,40 @@ +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/frontend/src/components/app/map/groups/GridGroup.js b/frontend/src/components/app/map/groups/GridGroup.js new file mode 100644 index 00000000..bbb1eb68 --- /dev/null +++ b/frontend/src/components/app/map/groups/GridGroup.js @@ -0,0 +1,41 @@ +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/frontend/src/components/app/map/groups/RackGroup.js b/frontend/src/components/app/map/groups/RackGroup.js new file mode 100644 index 00000000..69d6ac10 --- /dev/null +++ b/frontend/src/components/app/map/groups/RackGroup.js @@ -0,0 +1,43 @@ +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 >= 0) { + color = convertLoadToSimulationColor(rackLoad); + } + + return ( + + + + + + + + ); +}; + +RackGroup.propTypes = { + tile: Shapes.Tile +}; + +export default RackGroup; diff --git a/frontend/src/components/app/map/groups/RoomGroup.js b/frontend/src/components/app/map/groups/RoomGroup.js new file mode 100644 index 00000000..c8f0d3db --- /dev/null +++ b/frontend/src/components/app/map/groups/RoomGroup.js @@ -0,0 +1,56 @@ +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/frontend/src/components/app/map/groups/TileGroup.js b/frontend/src/components/app/map/groups/TileGroup.js new file mode 100644 index 00000000..8f3953d7 --- /dev/null +++ b/frontend/src/components/app/map/groups/TileGroup.js @@ -0,0 +1,43 @@ +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 >= 0) { + color = convertLoadToSimulationColor(roomLoad); + } + + return ( + onClick(tile)}> + + {tileObject} + + ); +}; + +TileGroup.propTypes = { + tile: Shapes.Tile, + newTile: PropTypes.bool +}; + +export default TileGroup; diff --git a/frontend/src/components/app/map/groups/WallGroup.js b/frontend/src/components/app/map/groups/WallGroup.js new file mode 100644 index 00000000..43de66e8 --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/layers/HoverLayerComponent.js b/frontend/src/components/app/map/layers/HoverLayerComponent.js new file mode 100644 index 00000000..c39532f1 --- /dev/null +++ b/frontend/src/components/app/map/layers/HoverLayerComponent.js @@ -0,0 +1,85 @@ +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/frontend/src/components/app/map/layers/MapLayerComponent.js b/frontend/src/components/app/map/layers/MapLayerComponent.js new file mode 100644 index 00000000..6ad3cb88 --- /dev/null +++ b/frontend/src/components/app/map/layers/MapLayerComponent.js @@ -0,0 +1,22 @@ +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/frontend/src/components/app/map/layers/ObjectHoverLayerComponent.js b/frontend/src/components/app/map/layers/ObjectHoverLayerComponent.js new file mode 100644 index 00000000..e7342d3c --- /dev/null +++ b/frontend/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/frontend/src/components/app/map/layers/RoomHoverLayerComponent.js b/frontend/src/components/app/map/layers/RoomHoverLayerComponent.js new file mode 100644 index 00000000..feea5ae5 --- /dev/null +++ b/frontend/src/components/app/map/layers/RoomHoverLayerComponent.js @@ -0,0 +1,6 @@ +import React from "react"; +import HoverLayerComponent from "./HoverLayerComponent"; + +const RoomHoverLayerComponent = props => ; + +export default RoomHoverLayerComponent; diff --git a/frontend/src/components/app/sidebars/Sidebar.js b/frontend/src/components/app/sidebars/Sidebar.js new file mode 100644 index 00000000..33dbe011 --- /dev/null +++ b/frontend/src/components/app/sidebars/Sidebar.js @@ -0,0 +1,50 @@ +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/frontend/src/components/app/sidebars/Sidebar.sass b/frontend/src/components/app/sidebars/Sidebar.sass new file mode 100644 index 00000000..4d0e5f1e --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/elements/LoadBarComponent.js b/frontend/src/components/app/sidebars/elements/LoadBarComponent.js new file mode 100644 index 00000000..8c9b164b --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/elements/LoadChartComponent.js b/frontend/src/components/app/sidebars/elements/LoadChartComponent.js new file mode 100644 index 00000000..5f0d40cb --- /dev/null +++ b/frontend/src/components/app/sidebars/elements/LoadChartComponent.js @@ -0,0 +1,90 @@ +import React from "react"; +import ReactDOM from "react-dom/server"; +import SvgSaver from "svgsaver"; +import { + VictoryAxis, + VictoryChart, + VictoryLabel, + VictoryLine, + VictoryScatter +} from "victory"; +import { convertSecondsToFormattedTime } from "../../../../util/date-time"; + +const LoadChartComponent = ({ data, currentTick }) => { + const onExport = () => { + const div = document.createElement("div"); + div.innerHTML = ReactDOM.renderToString( + + ); + div.firstChild.style = + "font-family: Roboto, Arial, sans-serif; font-size: 10pt;"; + const svgSaver = new SvgSaver(); + svgSaver.asSvg( + div.firstChild, + "opendc-chart-export-" + Date.now() + ".svg" + ); + }; + + return ( +
+ Load over time + + +
+ ); +}; + +const VictoryChartComponent = ({ data, currentTick, showCurrentTick }) => ( + + convertSecondsToFormattedTime(tick)} + fixLabelOverlap={true} + label="Simulated Time" + /> + + + + {showCurrentTick ? ( + + } + data={[{ x: currentTick + 1, y: 0 }, { x: currentTick + 1, y: 1 }]} + labels={point => + point.y === 1 + ? "Current tick : " + convertSecondsToFormattedTime(currentTick) + : ""} + style={{ + data: { stroke: "#00A6D6", strokeWidth: 4 }, + labels: { fill: "#00A6D6" } + }} + /> + ) : ( + undefined + )} + +); + +const ExportChartComponent = ({ onExport }) => ( + +); + +export default LoadChartComponent; diff --git a/frontend/src/components/app/sidebars/simulation/ExperimentMetadataComponent.js b/frontend/src/components/app/sidebars/simulation/ExperimentMetadataComponent.js new file mode 100644 index 00000000..bc563dab --- /dev/null +++ b/frontend/src/components/app/sidebars/simulation/ExperimentMetadataComponent.js @@ -0,0 +1,23 @@ +import React from "react"; + +const ExperimentMetadataComponent = ({ + experimentName, + pathName, + traceName, + schedulerName +}) => ( +
+

{experimentName}

+

+ Path: {pathName} +

+

+ Trace: {traceName} +

+

+ Scheduler: {schedulerName} +

+
+); + +export default ExperimentMetadataComponent; diff --git a/frontend/src/components/app/sidebars/simulation/LoadMetricComponent.js b/frontend/src/components/app/sidebars/simulation/LoadMetricComponent.js new file mode 100644 index 00000000..3e4cf810 --- /dev/null +++ b/frontend/src/components/app/sidebars/simulation/LoadMetricComponent.js @@ -0,0 +1,40 @@ +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/frontend/src/components/app/sidebars/simulation/SimulationSidebarComponent.js b/frontend/src/components/app/sidebars/simulation/SimulationSidebarComponent.js new file mode 100644 index 00000000..08dbb29a --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/simulation/SimulationSidebarComponent.sass b/frontend/src/components/app/sidebars/simulation/SimulationSidebarComponent.sass new file mode 100644 index 00000000..82af97fa --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/simulation/TaskComponent.js b/frontend/src/components/app/sidebars/simulation/TaskComponent.js new file mode 100644 index 00000000..bd917cc9 --- /dev/null +++ b/frontend/src/components/app/sidebars/simulation/TaskComponent.js @@ -0,0 +1,58 @@ +import approx from "approximate-number"; +import classNames from "classnames"; +import React from "react"; +import { convertSecondsToFormattedTime } from "../../../../util/date-time"; + +const TaskComponent = ({ task, flopsLeft }) => { + let icon; + let progressBarContent; + let percent; + let infoTitle; + + if (flopsLeft === task.totalFlopCount) { + icon = "hourglass-half"; + progressBarContent = ""; + percent = 0; + infoTitle = "Not submitted yet"; + } else if (flopsLeft > 0) { + icon = "refresh"; + progressBarContent = approx(task.totalFlopCount - flopsLeft) + " FLOP"; + percent = 100 * (task.totalFlopCount - flopsLeft) / task.totalFlopCount; + infoTitle = + progressBarContent + " (" + Math.round(percent * 10) / 10 + "%)"; + } else { + icon = "check"; + progressBarContent = "Completed"; + percent = 100; + infoTitle = "Completed"; + } + + return ( +
  • +
    +
    {approx(task.totalFlopCount)} FLOP
    + Starts at {convertSecondsToFormattedTime(task.startTick)} +
    +
    + +
    +
    + {progressBarContent} +
    +
    +
    +
  • + ); +}; + +export default TaskComponent; diff --git a/frontend/src/components/app/sidebars/simulation/TraceComponent.js b/frontend/src/components/app/sidebars/simulation/TraceComponent.js new file mode 100644 index 00000000..2b6559b4 --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/NameComponent.js b/frontend/src/components/app/sidebars/topology/NameComponent.js new file mode 100644 index 00000000..805538b3 --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/TopologySidebarComponent.js b/frontend/src/components/app/sidebars/topology/TopologySidebarComponent.js new file mode 100644 index 00000000..81e510a1 --- /dev/null +++ b/frontend/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 = ; + 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/frontend/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js b/frontend/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js new file mode 100644 index 00000000..f16c19f0 --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js @@ -0,0 +1,20 @@ +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/frontend/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js b/frontend/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js new file mode 100644 index 00000000..7b049642 --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js @@ -0,0 +1,31 @@ +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/frontend/src/components/app/sidebars/topology/machine/BackToRackComponent.js b/frontend/src/components/app/sidebars/topology/machine/BackToRackComponent.js new file mode 100644 index 00000000..7f56aca0 --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js b/frontend/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js new file mode 100644 index 00000000..d8774bf9 --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/machine/MachineNameComponent.js b/frontend/src/components/app/sidebars/topology/machine/MachineNameComponent.js new file mode 100644 index 00000000..0ad8b79c --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js b/frontend/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js new file mode 100644 index 00000000..5ccaf25c --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js @@ -0,0 +1,27 @@ +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/frontend/src/components/app/sidebars/topology/machine/UnitAddComponent.js b/frontend/src/components/app/sidebars/topology/machine/UnitAddComponent.js new file mode 100644 index 00000000..0c903228 --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/machine/UnitAddComponent.js @@ -0,0 +1,46 @@ +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/frontend/src/components/app/sidebars/topology/machine/UnitComponent.js b/frontend/src/components/app/sidebars/topology/machine/UnitComponent.js new file mode 100644 index 00000000..7c27043d --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/machine/UnitComponent.js @@ -0,0 +1,78 @@ +import React from "react"; +import jQuery from "../../../../../util/jquery"; + +class UnitComponent extends React.Component { + componentDidMount() { + jQuery(".unit-info-popover").popover({ + trigger: "focus" + }); + } + + render() { + let unitInfo; + if (this.props.unitType === "cpu" || this.props.unitType === "gpu") { + unitInfo = + "Clockrate: " + + this.props.unit.clockRateMhz + + " MHz
    " + + "Num. Cores: " + + this.props.unit.numberOfCores + + "
    " + + "Energy Cons.: " + + this.props.unit.energyConsumptionW + + " W"; + } else if ( + this.props.unitType === "memory" || + this.props.unitType === "storage" + ) { + unitInfo = + "Speed: " + + this.props.unit.speedMbPerS + + " Mb/s
    " + + "Size: " + + this.props.unit.sizeMb + + " MB
    " + + "Energy Cons.: " + + this.props.unit.energyConsumptionW + + " W"; + } + + return ( +
  • + + {this.props.unit.manufacturer + + " " + + this.props.unit.family + + " " + + this.props.unit.model + + " " + + this.props.unit.generation} + + + + {this.props.inSimulation ? ( + undefined + ) : ( + + + + )} + +
  • + ); + } +} + +export default UnitComponent; diff --git a/frontend/src/components/app/sidebars/topology/machine/UnitListComponent.js b/frontend/src/components/app/sidebars/topology/machine/UnitListComponent.js new file mode 100644 index 00000000..38df806b --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/machine/UnitListComponent.js @@ -0,0 +1,29 @@ +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/frontend/src/components/app/sidebars/topology/machine/UnitTabsComponent.js b/frontend/src/components/app/sidebars/topology/machine/UnitTabsComponent.js new file mode 100644 index 00000000..0683c796 --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/machine/UnitTabsComponent.js @@ -0,0 +1,65 @@ +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/frontend/src/components/app/sidebars/topology/rack/BackToRoomComponent.js b/frontend/src/components/app/sidebars/topology/rack/BackToRoomComponent.js new file mode 100644 index 00000000..6bcf4088 --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/rack/DeleteRackComponent.js b/frontend/src/components/app/sidebars/topology/rack/DeleteRackComponent.js new file mode 100644 index 00000000..d8aa7634 --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/rack/EmptySlotComponent.js b/frontend/src/components/app/sidebars/topology/rack/EmptySlotComponent.js new file mode 100644 index 00000000..d86f9fee --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/rack/EmptySlotComponent.js @@ -0,0 +1,19 @@ +import React from "react"; + +const EmptySlotComponent = ({ position, onAdd, inSimulation }) => ( +
  • + + {position} + + {inSimulation ? ( + Empty Slot + ) : ( + + )} +
  • +); + +export default EmptySlotComponent; diff --git a/frontend/src/components/app/sidebars/topology/rack/MachineComponent.js b/frontend/src/components/app/sidebars/topology/rack/MachineComponent.js new file mode 100644 index 00000000..2521f4a2 --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/rack/MachineComponent.js @@ -0,0 +1,78 @@ +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 >= 0) { + 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/frontend/src/components/app/sidebars/topology/rack/MachineListComponent.js b/frontend/src/components/app/sidebars/topology/rack/MachineListComponent.js new file mode 100644 index 00000000..d5521557 --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/rack/MachineListComponent.js @@ -0,0 +1,26 @@ +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/frontend/src/components/app/sidebars/topology/rack/MachineListComponent.sass b/frontend/src/components/app/sidebars/topology/rack/MachineListComponent.sass new file mode 100644 index 00000000..bbcfe696 --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/rack/MachineListComponent.sass @@ -0,0 +1,2 @@ +.machine-list li + min-height: 64px diff --git a/frontend/src/components/app/sidebars/topology/rack/RackNameComponent.js b/frontend/src/components/app/sidebars/topology/rack/RackNameComponent.js new file mode 100644 index 00000000..5e095823 --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/rack/RackSidebarComponent.js b/frontend/src/components/app/sidebars/topology/rack/RackSidebarComponent.js new file mode 100644 index 00000000..f832b9b9 --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/rack/RackSidebarComponent.js @@ -0,0 +1,34 @@ +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/frontend/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass b/frontend/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass new file mode 100644 index 00000000..822804bc --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/room/BackToBuildingComponent.js b/frontend/src/components/app/sidebars/topology/room/BackToBuildingComponent.js new file mode 100644 index 00000000..0409dbdd --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/room/DeleteRoomComponent.js b/frontend/src/components/app/sidebars/topology/room/DeleteRoomComponent.js new file mode 100644 index 00000000..3e3b3b36 --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/room/EditRoomComponent.js b/frontend/src/components/app/sidebars/topology/room/EditRoomComponent.js new file mode 100644 index 00000000..c3b9f0ad --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/room/EditRoomComponent.js @@ -0,0 +1,27 @@ +import classNames from "classnames"; +import React from "react"; + +const EditRoomComponent = ({ + onEdit, + onFinish, + isEditing, + isInRackConstructionMode +}) => + isEditing ? ( +
    + + Finish editing room +
    + ) : ( +
    (isInRackConstructionMode ? undefined : onEdit())} + > + + Edit the tiles of this room +
    + ); + +export default EditRoomComponent; diff --git a/frontend/src/components/app/sidebars/topology/room/RackConstructionComponent.js b/frontend/src/components/app/sidebars/topology/room/RackConstructionComponent.js new file mode 100644 index 00000000..06b8a2aa --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/room/RackConstructionComponent.js @@ -0,0 +1,32 @@ +import classNames from "classnames"; +import React from "react"; + +const RackConstructionComponent = ({ + onStart, + onStop, + inRackConstructionMode, + isEditingRoom +}) => { + if (inRackConstructionMode) { + return ( +
    + + Stop rack construction +
    + ); + } + + return ( +
    (isEditingRoom ? undefined : onStart())} + > + + Start rack construction +
    + ); +}; + +export default RackConstructionComponent; diff --git a/frontend/src/components/app/sidebars/topology/room/RoomNameComponent.js b/frontend/src/components/app/sidebars/topology/room/RoomNameComponent.js new file mode 100644 index 00000000..11b88edd --- /dev/null +++ b/frontend/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/frontend/src/components/app/sidebars/topology/room/RoomSidebarComponent.js b/frontend/src/components/app/sidebars/topology/room/RoomSidebarComponent.js new file mode 100644 index 00000000..275f9624 --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/room/RoomSidebarComponent.js @@ -0,0 +1,38 @@ +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 EditRoomContainer from "../../../../../containers/app/sidebars/topology/room/EditRoomContainer"; +import RackConstructionContainer from "../../../../../containers/app/sidebars/topology/room/RackConstructionContainer"; +import RoomNameContainer from "../../../../../containers/app/sidebars/topology/room/RoomNameContainer"; +import 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/frontend/src/components/app/sidebars/topology/room/RoomTypeComponent.js b/frontend/src/components/app/sidebars/topology/room/RoomTypeComponent.js new file mode 100644 index 00000000..46d91c2c --- /dev/null +++ b/frontend/src/components/app/sidebars/topology/room/RoomTypeComponent.js @@ -0,0 +1,8 @@ +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/frontend/src/components/app/timeline/PlayButtonComponent.js b/frontend/src/components/app/timeline/PlayButtonComponent.js new file mode 100644 index 00000000..1a9b0ced --- /dev/null +++ b/frontend/src/components/app/timeline/PlayButtonComponent.js @@ -0,0 +1,30 @@ +import React from "react"; + +const PlayButtonComponent = ({ + isPlaying, + currentTick, + lastSimulatedTick, + onPlay, + onPause +}) => ( +
    { + if (isPlaying) { + onPause(); + } else { + if (currentTick !== lastSimulatedTick) { + onPlay(); + } + } + }} + > + {isPlaying ? ( + + ) : ( + + )} +
    +); + +export default PlayButtonComponent; diff --git a/frontend/src/components/app/timeline/Timeline.sass b/frontend/src/components/app/timeline/Timeline.sass new file mode 100644 index 00000000..4c99a218 --- /dev/null +++ b/frontend/src/components/app/timeline/Timeline.sass @@ -0,0 +1,116 @@ +@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 + + pointer-events: none + + .section-marker + position: absolute + top: 0 + left: 0 + + width: 3px + height: 100% + + background: #222222 + + z-index: 504 + + pointer-events: none diff --git a/frontend/src/components/app/timeline/TimelineComponent.js b/frontend/src/components/app/timeline/TimelineComponent.js new file mode 100644 index 00000000..0f88b8f4 --- /dev/null +++ b/frontend/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/frontend/src/components/app/timeline/TimelineControlsComponent.js b/frontend/src/components/app/timeline/TimelineControlsComponent.js new file mode 100644 index 00000000..f3d55154 --- /dev/null +++ b/frontend/src/components/app/timeline/TimelineControlsComponent.js @@ -0,0 +1,49 @@ +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/frontend/src/components/app/timeline/TimelineLabelsComponent.js b/frontend/src/components/app/timeline/TimelineLabelsComponent.js new file mode 100644 index 00000000..6943a86f --- /dev/null +++ b/frontend/src/components/app/timeline/TimelineLabelsComponent.js @@ -0,0 +1,15 @@ +import React from "react"; +import { convertSecondsToFormattedTime } from "../../../util/date-time"; + +const TimelineLabelsComponent = ({ currentTick, lastSimulatedTick }) => ( +
    +
    + {convertSecondsToFormattedTime(currentTick)} +
    +
    + {convertSecondsToFormattedTime(lastSimulatedTick)} +
    +
    +); + +export default TimelineLabelsComponent; diff --git a/frontend/src/components/experiments/ExperimentListComponent.js b/frontend/src/components/experiments/ExperimentListComponent.js new file mode 100644 index 00000000..2f7106e5 --- /dev/null +++ b/frontend/src/components/experiments/ExperimentListComponent.js @@ -0,0 +1,59 @@ +import PropTypes from "prop-types"; +import React from "react"; +import ExperimentRowContainer from "../../containers/experiments/ExperimentRowContainer"; + +const ExperimentListComponent = ({ experimentIds, loading }) => { + let alert; + + if (loading) { + alert = ( +
    + + Loading Experiments... +
    + ); + } else if (experimentIds.length === 0 && !loading) { + alert = ( +
    + + No experiments here yet... Add some with the button + below! +
    + ); + } + + return ( +
    + {alert ? ( + alert + ) : ( + + + + + + + + + + + {experimentIds.map(experimentId => ( + + ))} + +
    NamePathTraceScheduler +
    + )} +
    + ); +}; + +ExperimentListComponent.propTypes = { + experimentIds: PropTypes.arrayOf(PropTypes.number).isRequired, + loading: PropTypes.bool +}; + +export default ExperimentListComponent; diff --git a/frontend/src/components/experiments/ExperimentRowComponent.js b/frontend/src/components/experiments/ExperimentRowComponent.js new file mode 100644 index 00000000..e71c6a00 --- /dev/null +++ b/frontend/src/components/experiments/ExperimentRowComponent.js @@ -0,0 +1,40 @@ +import PropTypes from "prop-types"; +import React from "react"; +import { Link } from "react-router-dom"; +import Shapes from "../../shapes/index"; + +const ExperimentRowComponent = ({ experiment, simulationId, onDelete }) => ( + + {experiment.name} + + {experiment.path.name + ? experiment.path.name + : "Path " + experiment.path.id} + + {experiment.trace.name} + {experiment.scheduler.name} + + + + +
    onDelete(experiment.id)} + > + +
    + + +); + +ExperimentRowComponent.propTypes = { + experiment: Shapes.Experiment.isRequired, + simulationId: PropTypes.number.isRequired +}; + +export default ExperimentRowComponent; diff --git a/frontend/src/components/experiments/NewExperimentButtonComponent.js b/frontend/src/components/experiments/NewExperimentButtonComponent.js new file mode 100644 index 00000000..651172e3 --- /dev/null +++ b/frontend/src/components/experiments/NewExperimentButtonComponent.js @@ -0,0 +1,17 @@ +import PropTypes from "prop-types"; +import React from "react"; + +const NewExperimentButtonComponent = ({ onClick }) => ( +
    +
    + + New Experiment +
    +
    +); + +NewExperimentButtonComponent.propTypes = { + onClick: PropTypes.func.isRequired +}; + +export default NewExperimentButtonComponent; diff --git a/frontend/src/components/home/ContactSection.js b/frontend/src/components/home/ContactSection.js new file mode 100644 index 00000000..4e640924 --- /dev/null +++ b/frontend/src/components/home/ContactSection.js @@ -0,0 +1,64 @@ +import React from "react"; +import FontAwesome from "react-fontawesome"; +import "./ContactSection.css"; +import ContentSection from "./ContentSection"; + +const ContactSection = () => ( + +
    +
    + + + + +
    +
    + TU Delft +
    +
    +
    +
    + A project by the   + + @Large Research Group + . +
    +
    +
    +
    + +
    + OpenDC is an experimental tool. Your data may get lost, overwritten, or + otherwise become unavailable. +
    + The OpenDC authors should in no way be liable in the event this happens + (see our{" "} + + + license + + ). Sorry for the inconvenience. +
    +
    + +); + +export default ContactSection; diff --git a/frontend/src/components/home/ContactSection.sass b/frontend/src/components/home/ContactSection.sass new file mode 100644 index 00000000..2cde7391 --- /dev/null +++ b/frontend/src/components/home/ContactSection.sass @@ -0,0 +1,15 @@ +.contact-section + background-color: #444 + color: #ddd + + a + color: #ddd + + a:hover + color: #fff + + .tudelft-icon + height: 100px + + .disclaimer + color: #cccccc diff --git a/frontend/src/components/home/ContentSection.js b/frontend/src/components/home/ContentSection.js new file mode 100644 index 00000000..2e24ee10 --- /dev/null +++ b/frontend/src/components/home/ContentSection.js @@ -0,0 +1,19 @@ +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React from "react"; +import "./ContentSection.css"; + +const ContentSection = ({ name, title, children }) => ( +
    +
    +

    {title}

    + {children} +
    +
    +); + +ContentSection.propTypes = { + name: PropTypes.string.isRequired +}; + +export default ContentSection; diff --git a/frontend/src/components/home/ContentSection.sass b/frontend/src/components/home/ContentSection.sass new file mode 100644 index 00000000..67541179 --- /dev/null +++ b/frontend/src/components/home/ContentSection.sass @@ -0,0 +1,9 @@ +@import ../../style-globals/_variables.sass + +.content-section + padding-top: 50px + padding-bottom: 100px + text-align: center + + h1 + margin-bottom: 30px diff --git a/frontend/src/components/home/IntroSection.js b/frontend/src/components/home/IntroSection.js new file mode 100644 index 00000000..59f5face --- /dev/null +++ b/frontend/src/components/home/IntroSection.js @@ -0,0 +1,40 @@ +import React from "react"; + +const IntroSection = () => ( +
    +
    +
    +
    +

    The datacenter (DC) industry...

    +
      +
    • Is worth over $15 bn, and growing
    • +
    • Has many hard-to-grasp concepts
    • +
    • Needs to become accessible to many
    • +
    +
    + +
    +

    OpenDC provides...

    +
      +
    • Collaborative online DC modeling
    • +
    • Diverse and effective DC simulation
    • +
    • Exploratory DC performance feedback
    • +
    +
    +
    +
    +
    +); + +export default IntroSection; diff --git a/frontend/src/components/home/JumbotronHeader.js b/frontend/src/components/home/JumbotronHeader.js new file mode 100644 index 00000000..8a5312b3 --- /dev/null +++ b/frontend/src/components/home/JumbotronHeader.js @@ -0,0 +1,20 @@ +import React from "react"; +import "./JumbotronHeader.css"; + +const JumbotronHeader = () => ( +
    +
    +
    +

    + OpenDC +

    +

    + Collaborative Datacenter Simulation and Exploration for Everybody +

    + OpenDC +
    +
    +
    +); + +export default JumbotronHeader; diff --git a/frontend/src/components/home/JumbotronHeader.sass b/frontend/src/components/home/JumbotronHeader.sass new file mode 100644 index 00000000..b88b79f7 --- /dev/null +++ b/frontend/src/components/home/JumbotronHeader.sass @@ -0,0 +1,24 @@ +.jumbotron-header + background: #00A6D6 + +.jumbotron + background-color: inherit + margin-bottom: 0 + + padding-top: 120px + padding-bottom: 120px + + img + max-width: 110px + + h1 + color: #fff + font-size: 4.5em + + .dc + color: #fff + font-weight: bold + + .lead + color: #fff + font-size: 1.4em diff --git a/frontend/src/components/home/ModelingSection.js b/frontend/src/components/home/ModelingSection.js new file mode 100644 index 00000000..17834b0b --- /dev/null +++ b/frontend/src/components/home/ModelingSection.js @@ -0,0 +1,24 @@ +import React from "react"; +import ScreenshotSection from "./ScreenshotSection"; + +const ModelingSection = () => ( + +

    Collaboratively...

    +
      +
    • Model DC layout, and room locations and types
    • +
    • Place racks in rooms and nodes in racks
    • +
    • + Add real-world CPU, GPU, memory, storage and network units to each node +
    • +
    • Select from diverse scheduling policies
    • +
    +
    +); + +export default ModelingSection; diff --git a/frontend/src/components/home/ScreenshotSection.js b/frontend/src/components/home/ScreenshotSection.js new file mode 100644 index 00000000..42b8ac77 --- /dev/null +++ b/frontend/src/components/home/ScreenshotSection.js @@ -0,0 +1,32 @@ +import classNames from "classnames"; +import React from "react"; +import ContentSection from "./ContentSection"; +import "./ScreenshotSection.css"; + +const ScreenshotSection = ({ + name, + title, + imageUrl, + caption, + imageIsRight, + children +}) => ( + +
    +
    + {children} +
    +
    + {caption} +
    {caption}
    +
    +
    +
    +); + +export default ScreenshotSection; diff --git a/frontend/src/components/home/ScreenshotSection.sass b/frontend/src/components/home/ScreenshotSection.sass new file mode 100644 index 00000000..a349ad48 --- /dev/null +++ b/frontend/src/components/home/ScreenshotSection.sass @@ -0,0 +1,5 @@ +.screenshot + outline: 2px black solid + padding-left: 0 + padding-right: 0 + margin-bottom: 5px diff --git a/frontend/src/components/home/SimulationSection.js b/frontend/src/components/home/SimulationSection.js new file mode 100644 index 00000000..3961e549 --- /dev/null +++ b/frontend/src/components/home/SimulationSection.js @@ -0,0 +1,25 @@ +import React from "react"; +import ScreenshotSection from "./ScreenshotSection"; + +const ModelingSection = () => ( + +

    Working with OpenDC:

    +
      +
    • Seamlessly switch between construction and simulation modes
    • +
    • + Choose one of several predefined workloads (Big Data, Bag of Tasks, + Hadoop, etc.) +
    • +
    • Play, pause, and skip around the informative simulation timeline
    • +
    • Visualize and demo live
    • +
    +
    +); + +export default ModelingSection; diff --git a/frontend/src/components/home/StakeholderSection.js b/frontend/src/components/home/StakeholderSection.js new file mode 100644 index 00000000..6d25fd86 --- /dev/null +++ b/frontend/src/components/home/StakeholderSection.js @@ -0,0 +1,42 @@ +import React from "react"; +import ContentSection from "./ContentSection"; + +const Stakeholder = ({ name, title, subtitle }) => ( +
    + {title} +
    +

    {title}

    +

    {subtitle}

    +
    +
    +); + +const StakeholderSection = () => ( + +
    + + + + + +
    +
    +); + +export default StakeholderSection; diff --git a/frontend/src/components/home/TeamSection.js b/frontend/src/components/home/TeamSection.js new file mode 100644 index 00000000..b86655b4 --- /dev/null +++ b/frontend/src/components/home/TeamSection.js @@ -0,0 +1,56 @@ +import React from "react"; +import ContentSection from "./ContentSection"; + +const TeamMember = ({ photoId, name, description }) => ( +
    + {name} +
    +

    {name}

    +
    {description}
    +
    +
    +); + +const TeamSection = () => ( + +
    + + + + +
    +
    + See{" "} + + atlarge.science/opendc + {" "} + for the full team! +
    +
    +); + +export default TeamSection; diff --git a/frontend/src/components/home/TechnologiesSection.js b/frontend/src/components/home/TechnologiesSection.js new file mode 100644 index 00000000..fdcfc522 --- /dev/null +++ b/frontend/src/components/home/TechnologiesSection.js @@ -0,0 +1,42 @@ +import React from "react"; +import FontAwesome from "react-fontawesome"; +import ContentSection from "./ContentSection"; + +const TechnologiesSection = () => ( + +
      +
    • + + + Browser + + JavaScript, React, Redux, Konva +
    • +
    • + + + Server + + + Python, Flask, FlaskSocketIO, OpenAPI + +
    • +
    • + + + Database + + MariaDB +
    • +
    • + + + Simulator + + Kotlin +
    • +
    +
    +); + +export default TechnologiesSection; diff --git a/frontend/src/components/modals/ConfirmationModal.js b/frontend/src/components/modals/ConfirmationModal.js new file mode 100644 index 00000000..abdce5ac --- /dev/null +++ b/frontend/src/components/modals/ConfirmationModal.js @@ -0,0 +1,37 @@ +import PropTypes from "prop-types"; +import React from "react"; +import Modal from "./Modal"; + +class ConfirmationModal extends React.Component { + static propTypes = { + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + show: PropTypes.bool.isRequired, + callback: PropTypes.func.isRequired + }; + + onConfirm() { + this.props.callback(true); + } + + onCancel() { + this.props.callback(false); + } + + render() { + return ( + + {this.props.message} + + ); + } +} + +export default ConfirmationModal; diff --git a/frontend/src/components/modals/Modal.js b/frontend/src/components/modals/Modal.js new file mode 100644 index 00000000..19337db8 --- /dev/null +++ b/frontend/src/components/modals/Modal.js @@ -0,0 +1,132 @@ +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React from "react"; +import jQuery from "../../util/jquery"; + +class Modal extends React.Component { + static propTypes = { + title: PropTypes.string.isRequired, + show: PropTypes.bool.isRequired, + onSubmit: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + submitButtonType: PropTypes.string, + submitButtonText: PropTypes.string + }; + static defaultProps = { + submitButtonType: "primary", + submitButtonText: "Save" + }; + static idCounter = 0; + + // Local, up-to-date copy of modal visibility for time between close event and a props update (to prevent duplicate + // 'close' triggers) + visible = false; + + constructor(props) { + super(props); + this.id = "modal-" + Modal.idCounter++; + } + + componentDidMount() { + this.visible = this.props.show; + this.openOrCloseModal(); + + // Trigger auto-focus + jQuery("#" + this.id) + .on("shown.bs.modal", function() { + jQuery(this) + .find("input") + .first() + .focus(); + }) + .on("hide.bs.modal", () => { + if (this.visible) { + this.props.onCancel(); + } + }) + .on("keydown", e => { + e.stopPropagation(); + }); + } + + componentDidUpdate() { + this.visible = this.props.show; + this.openOrCloseModal(); + } + + onSubmit() { + if (this.visible) { + this.props.onSubmit(); + this.visible = false; + this.closeModal(); + } + } + + onCancel() { + if (this.visible) { + this.props.onCancel(); + this.visible = false; + this.closeModal(); + } + } + + openModal() { + jQuery("#" + this.id).modal("show"); + } + + closeModal() { + jQuery("#" + this.id).modal("hide"); + } + + openOrCloseModal() { + if (this.visible) { + this.openModal(); + } else { + this.closeModal(); + } + } + + render() { + return ( + + ); + } +} + +export default Modal; diff --git a/frontend/src/components/modals/TextInputModal.js b/frontend/src/components/modals/TextInputModal.js new file mode 100644 index 00000000..cc16f8e1 --- /dev/null +++ b/frontend/src/components/modals/TextInputModal.js @@ -0,0 +1,58 @@ +import PropTypes from "prop-types"; +import React from "react"; +import Modal from "./Modal"; + +class TextInputModal extends React.Component { + static propTypes = { + title: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + show: PropTypes.bool.isRequired, + callback: PropTypes.func.isRequired, + initialValue: PropTypes.string + }; + + componentDidUpdate() { + if (this.props.initialValue) { + this.textInput.value = this.props.initialValue; + } + } + + onSubmit() { + this.props.callback(this.textInput.value); + this.textInput.value = ""; + } + + onCancel() { + this.props.callback(undefined); + this.textInput.value = ""; + } + + render() { + return ( + +
    { + e.preventDefault(); + this.onSubmit(); + }} + > +
    + + (this.textInput = textInput)} + /> +
    +
    +
    + ); + } +} + +export default TextInputModal; diff --git a/frontend/src/components/modals/custom-components/NewExperimentModalComponent.js b/frontend/src/components/modals/custom-components/NewExperimentModalComponent.js new file mode 100644 index 00000000..e356fe96 --- /dev/null +++ b/frontend/src/components/modals/custom-components/NewExperimentModalComponent.js @@ -0,0 +1,104 @@ +import PropTypes from "prop-types"; +import React from "react"; +import Shapes from "../../../shapes"; +import Modal from "../Modal"; + +class NewExperimentModalComponent extends React.Component { + static propTypes = { + show: PropTypes.bool.isRequired, + paths: PropTypes.arrayOf(Shapes.Path), + schedulers: PropTypes.arrayOf(Shapes.Scheduler), + traces: PropTypes.arrayOf(Shapes.Trace), + callback: PropTypes.func.isRequired + }; + + reset() { + this.textInput.value = ""; + this.pathSelect.selectedIndex = 0; + this.traceSelect.selectedIndex = 0; + this.schedulerSelect.selectedIndex = 0; + } + + onSubmit() { + this.props.callback( + this.textInput.value, + parseInt(this.pathSelect.value, 10), + parseInt(this.traceSelect.value, 10), + this.schedulerSelect.value + ); + this.reset(); + } + + onCancel() { + this.props.callback(undefined); + this.reset(); + } + + render() { + return ( + +
    { + e.preventDefault(); + this.onSubmit(); + }} + > +
    + + (this.textInput = textInput)} + /> +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + ); + } +} + +export default NewExperimentModalComponent; diff --git a/frontend/src/components/navigation/AppNavbar.js b/frontend/src/components/navigation/AppNavbar.js new file mode 100644 index 00000000..1a35f85d --- /dev/null +++ b/frontend/src/components/navigation/AppNavbar.js @@ -0,0 +1,56 @@ +import React from "react"; +import FontAwesome from "react-fontawesome"; +import { Link } from "react-router-dom"; +import Navbar, { NavItem } from "./Navbar"; +import "./Navbar.css"; + +const AppNavbar = ({ simulationId, inSimulation, fullWidth }) => ( + + {inSimulation ? ( + + + + Construction + + + ) : ( + undefined + )} + {inSimulation ? ( + + + + Experiments + + + ) : ( + undefined + )} + + + + My Simulations + + + + + + Support + + + +); + +export default AppNavbar; diff --git a/frontend/src/components/navigation/HomeNavbar.js b/frontend/src/components/navigation/HomeNavbar.js new file mode 100644 index 00000000..5d08bf3c --- /dev/null +++ b/frontend/src/components/navigation/HomeNavbar.js @@ -0,0 +1,24 @@ +import React from "react"; +import Navbar from "./Navbar"; +import "./Navbar.css"; + +const ScrollNavItem = ({ id, name }) => ( +
  • + + {name} + +
  • +); + +const HomeNavbar = () => ( + + + + + + + + +); + +export default HomeNavbar; diff --git a/frontend/src/components/navigation/LogoutButton.js b/frontend/src/components/navigation/LogoutButton.js new file mode 100644 index 00000000..800a3da8 --- /dev/null +++ b/frontend/src/components/navigation/LogoutButton.js @@ -0,0 +1,16 @@ +import PropTypes from "prop-types"; +import React from "react"; +import FontAwesome from "react-fontawesome"; +import { Link } from "react-router-dom"; + +const LogoutButton = ({ onLogout }) => ( + + + +); + +LogoutButton.propTypes = { + onLogout: PropTypes.func.isRequired +}; + +export default LogoutButton; diff --git a/frontend/src/components/navigation/Navbar.js b/frontend/src/components/navigation/Navbar.js new file mode 100644 index 00000000..44458949 --- /dev/null +++ b/frontend/src/components/navigation/Navbar.js @@ -0,0 +1,102 @@ +import classNames from "classnames"; +import React from "react"; +import { Link, withRouter } from "react-router-dom"; +import { userIsLoggedIn } from "../../auth/index"; +import Login from "../../containers/auth/Login"; +import Logout from "../../containers/auth/Logout"; +import ProfileName from "../../containers/auth/ProfileName"; +import "./Navbar.css"; + +export const NAVBAR_HEIGHT = 60; + +export const NavItem = withRouter(props => ); +export const LoggedInSection = withRouter(props => ( + +)); + +const GitHubLink = () => ( + + + +); + +const NavItemWithoutRoute = ({ route, location, children }) => ( +
  • + {children} +
  • +); + +const LoggedInSectionWithoutRoute = ({ location }) => ( +
      + {userIsLoggedIn() ? ( + [ + location.pathname === "/" ? ( + + + My Simulations + + + ) : ( + + + + + + ), + + + + ] + ) : ( + + + + + )} +
    +); + +const Navbar = ({ fullWidth, children }) => ( + +); + +export default Navbar; diff --git a/frontend/src/components/navigation/Navbar.sass b/frontend/src/components/navigation/Navbar.sass new file mode 100644 index 00000000..94c52936 --- /dev/null +++ b/frontend/src/components/navigation/Navbar.sass @@ -0,0 +1,29 @@ +@import ../../style-globals/_mixins.sass +@import ../../style-globals/_variables.sass + +.navbar + border-top: $blue 3px solid + border-bottom: $gray-semi-dark 1px solid + color: $gray-very-dark + background: #fafafb + +.opendc-brand + display: inline-block + color: $gray-very-dark + + +transition(background, $transition-length) + + img + position: relative + bottom: 3px + display: inline-block + width: 30px + +.login + height: 40px + background: $blue + border: none + +clickable + + &:hover + background: $blue-dark diff --git a/frontend/src/components/not-found/BlinkingCursor.js b/frontend/src/components/not-found/BlinkingCursor.js new file mode 100644 index 00000000..eea89e7b --- /dev/null +++ b/frontend/src/components/not-found/BlinkingCursor.js @@ -0,0 +1,6 @@ +import React from "react"; +import "./BlinkingCursor.css"; + +const BlinkingCursor = () => _; + +export default BlinkingCursor; diff --git a/frontend/src/components/not-found/BlinkingCursor.sass b/frontend/src/components/not-found/BlinkingCursor.sass new file mode 100644 index 00000000..6be1476d --- /dev/null +++ b/frontend/src/components/not-found/BlinkingCursor.sass @@ -0,0 +1,35 @@ +.blinking-cursor + -webkit-animation: 1s blink step-end infinite + -moz-animation: 1s blink step-end infinite + -o-animation: 1s blink step-end infinite + animation: 1s blink step-end infinite + +@keyframes blink + from, to + color: #eeeeee + 50% + color: #333333 + +@-moz-keyframes blink + from, to + color: #eeeeee + 50% + color: #333333 + +@-webkit-keyframes blink + from, to + color: #eeeeee + 50% + color: #333333 + +@-ms-keyframes blink + from, to + color: #eeeeee + 50% + color: #333333 + +@-o-keyframes blink + from, to + color: #eeeeee + 50% + color: #333333 diff --git a/frontend/src/components/not-found/CodeBlock.js b/frontend/src/components/not-found/CodeBlock.js new file mode 100644 index 00000000..46dc4402 --- /dev/null +++ b/frontend/src/components/not-found/CodeBlock.js @@ -0,0 +1,34 @@ +import React from "react"; +import "./CodeBlock.css"; + +const CodeBlock = () => { + const textBlock = + " oo oooo oo
    " + + " oo oo oo oo
    " + + " oo oo oo oo
    " + + " oooooo oo oo oooooo
    " + + " oo oo oo oo
    " + + " oo oooo oo
    "; + const charList = textBlock.split(""); + + // Binary representation of the string "OpenDC!" ;) + const binaryString = + "01001111011100000110010101101110010001000100001100100001"; + + let binaryIndex = 0; + for (let i = 0; i < charList.length; i++) { + if (charList[i] === "o") { + charList[i] = binaryString[binaryIndex]; + binaryIndex++; + } + } + + return ( +
    + ); +}; + +export default CodeBlock; diff --git a/frontend/src/components/not-found/CodeBlock.sass b/frontend/src/components/not-found/CodeBlock.sass new file mode 100644 index 00000000..51a3d3d0 --- /dev/null +++ b/frontend/src/components/not-found/CodeBlock.sass @@ -0,0 +1,3 @@ +.code-block + white-space: pre-wrap + margin-top: 60px diff --git a/frontend/src/components/not-found/TerminalWindow.js b/frontend/src/components/not-found/TerminalWindow.js new file mode 100644 index 00000000..c6b8b78b --- /dev/null +++ b/frontend/src/components/not-found/TerminalWindow.js @@ -0,0 +1,29 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import BlinkingCursor from "./BlinkingCursor"; +import CodeBlock from "./CodeBlock"; +import "./TerminalWindow.css"; + +const TerminalWindow = () => ( +
    +
    Terminal -- bash
    +
    +
    + $ status
    + opendc[4264]: segfault at 0000051497be459d1 err 12 in libopendc.9.0.4
    + opendc[4269]: segfault at 000004234855fc2db err 3 in libopendc.9.0.4
    + opendc[4270]: STDERR Page does not exist
    +
    + +
    + Got lost? +
    + + GET ME BACK TO OPENDC + +
    +
    +); + +export default TerminalWindow; diff --git a/frontend/src/components/not-found/TerminalWindow.sass b/frontend/src/components/not-found/TerminalWindow.sass new file mode 100644 index 00000000..4f51a77f --- /dev/null +++ b/frontend/src/components/not-found/TerminalWindow.sass @@ -0,0 +1,70 @@ +.terminal-window + width: 600px + height: 400px + display: block + + position: absolute + top: 0 + bottom: 0 + left: 0 + right: 0 + + margin: auto + + -webkit-user-select: none + -moz-user-select: none + -ms-user-select: none + user-select: none + cursor: default + + overflow: hidden + + box-shadow: 5px 5px 20px #444444 + +.terminal-header + font-family: monospace + background: #cccccc + color: #444444 + height: 30px + line-height: 30px + padding-left: 10px + + border-top-left-radius: 7px + border-top-right-radius: 7px + +.terminal-body + font-family: monospace + text-align: center + background-color: #333333 + color: #eeeeee + padding: 10px + + height: 100% + +.segfault + text-align: left + +.sub-title + margin-top: 20px + +.home-btn + margin-top: 10px + padding: 5px + display: inline-block + border: 1px solid #eeeeee + color: #eeeeee + text-decoration: none + cursor: pointer + + -webkit-transition: all 200ms + -moz-transition: all 200ms + -o-transition: all 200ms + transition: all 200ms + +.home-btn:hover + background: #eeeeee + color: #333333 + +.home-btn:active + background: #333333 + color: #eeeeee diff --git a/frontend/src/components/simulations/FilterButton.js b/frontend/src/components/simulations/FilterButton.js new file mode 100644 index 00000000..aa41f180 --- /dev/null +++ b/frontend/src/components/simulations/FilterButton.js @@ -0,0 +1,24 @@ +import classNames from "classnames"; +import PropTypes from "prop-types"; +import React from "react"; + +const FilterButton = ({ active, children, onClick }) => ( + +); + +FilterButton.propTypes = { + active: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, + onClick: PropTypes.func.isRequired +}; + +export default FilterButton; diff --git a/frontend/src/components/simulations/FilterPanel.js b/frontend/src/components/simulations/FilterPanel.js new file mode 100644 index 00000000..836c0842 --- /dev/null +++ b/frontend/src/components/simulations/FilterPanel.js @@ -0,0 +1,13 @@ +import React from "react"; +import FilterLink from "../../containers/simulations/FilterLink"; +import "./FilterPanel.css"; + +const FilterPanel = () => ( +
    + All Simulations + My Simulations + Shared with me +
    +); + +export default FilterPanel; diff --git a/frontend/src/components/simulations/FilterPanel.sass b/frontend/src/components/simulations/FilterPanel.sass new file mode 100644 index 00000000..e10e4746 --- /dev/null +++ b/frontend/src/components/simulations/FilterPanel.sass @@ -0,0 +1,5 @@ +.filter-panel + display: flex + + button + flex: 1 !important diff --git a/frontend/src/components/simulations/NewSimulationButtonComponent.js b/frontend/src/components/simulations/NewSimulationButtonComponent.js new file mode 100644 index 00000000..7e12d30f --- /dev/null +++ b/frontend/src/components/simulations/NewSimulationButtonComponent.js @@ -0,0 +1,17 @@ +import PropTypes from "prop-types"; +import React from "react"; + +const NewSimulationButtonComponent = ({ onClick }) => ( +
    +
    + + New Simulation +
    +
    +); + +NewSimulationButtonComponent.propTypes = { + onClick: PropTypes.func.isRequired +}; + +export default NewSimulationButtonComponent; diff --git a/frontend/src/components/simulations/SimulationActionButtons.js b/frontend/src/components/simulations/SimulationActionButtons.js new file mode 100644 index 00000000..46f4f159 --- /dev/null +++ b/frontend/src/components/simulations/SimulationActionButtons.js @@ -0,0 +1,37 @@ +import PropTypes from "prop-types"; +import React from "react"; +import { Link } from "react-router-dom"; + +const SimulationActionButtons = ({ simulationId, onViewUsers, onDelete }) => ( + + + + +
    onViewUsers(simulationId)} + > + +
    +
    onDelete(simulationId)} + > + +
    + +); + +SimulationActionButtons.propTypes = { + simulationId: PropTypes.number.isRequired, + onViewUsers: PropTypes.func, + onDelete: PropTypes.func +}; + +export default SimulationActionButtons; diff --git a/frontend/src/components/simulations/SimulationAuthList.js b/frontend/src/components/simulations/SimulationAuthList.js new file mode 100644 index 00000000..f29dc96d --- /dev/null +++ b/frontend/src/components/simulations/SimulationAuthList.js @@ -0,0 +1,43 @@ +import PropTypes from "prop-types"; +import React from "react"; +import Shapes from "../../shapes/index"; +import SimulationAuthRow from "./SimulationAuthRow"; + +const SimulationAuthList = ({ authorizations }) => { + return ( +
    + {authorizations.length === 0 ? ( +
    + + No simulations here yet... Add some with the 'New + Simulation' button! +
    + ) : ( + + + + + + + + + + {authorizations.map(authorization => ( + + ))} + +
    Simulation nameLast editedAccess rights +
    + )} +
    + ); +}; + +SimulationAuthList.propTypes = { + authorizations: PropTypes.arrayOf(Shapes.Authorization).isRequired +}; + +export default SimulationAuthList; diff --git a/frontend/src/components/simulations/SimulationAuthRow.js b/frontend/src/components/simulations/SimulationAuthRow.js new file mode 100644 index 00000000..b638fbce --- /dev/null +++ b/frontend/src/components/simulations/SimulationAuthRow.js @@ -0,0 +1,32 @@ +import classNames from "classnames"; +import React from "react"; +import SimulationActions from "../../containers/simulations/SimulationActions"; +import Shapes from "../../shapes/index"; +import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from "../../util/authorizations"; +import { parseAndFormatDateTime } from "../../util/date-time"; + +const SimulationAuthRow = ({ simulationAuth }) => ( + + {simulationAuth.simulation.name} + + {parseAndFormatDateTime(simulationAuth.simulation.datetimeLastEdited)} + + + + {AUTH_DESCRIPTION_MAP[simulationAuth.authorizationLevel]} + + + +); + +SimulationAuthRow.propTypes = { + simulationAuth: Shapes.Authorization.isRequired +}; + +export default SimulationAuthRow; -- cgit v1.2.3