summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-04-25 16:01:14 +0200
committerFabian Mastenbroek <mail.fabianm@gmail.com>2021-04-25 16:01:14 +0200
commitcd0b45627f0d8da8c8dc4edde223f3c36e9bcfbf (patch)
tree6ae1681630a0e270c23804e6dbb3bd414ebe5d6e /opendc-web/opendc-web-ui/src
parent128a1db017545597a5c035b7960eb3fd36b5f987 (diff)
build: Migrate to flat project structure
This change updates the project structure to become flattened. Previously, the simulator, frontend and API each lived into their own directory. With this change, all modules of the project live in the top-level directory of the repository. This should improve discoverability of modules of the project.
Diffstat (limited to 'opendc-web/opendc-web-ui/src')
-rw-r--r--opendc-web/opendc-web-ui/src/actions/auth.js23
-rw-r--r--opendc-web/opendc-web-ui/src/actions/interaction-level.js50
-rw-r--r--opendc-web/opendc-web-ui/src/actions/map.js82
-rw-r--r--opendc-web/opendc-web-ui/src/actions/modals/portfolios.js14
-rw-r--r--opendc-web/opendc-web-ui/src/actions/modals/prefabs.js14
-rw-r--r--opendc-web/opendc-web-ui/src/actions/modals/profile.js14
-rw-r--r--opendc-web/opendc-web-ui/src/actions/modals/projects.js14
-rw-r--r--opendc-web/opendc-web-ui/src/actions/modals/scenarios.js14
-rw-r--r--opendc-web/opendc-web-ui/src/actions/modals/topology.js84
-rw-r--r--opendc-web/opendc-web-ui/src/actions/objects.js41
-rw-r--r--opendc-web/opendc-web-ui/src/actions/portfolios.js41
-rw-r--r--opendc-web/opendc-web-ui/src/actions/prefabs.js32
-rw-r--r--opendc-web/opendc-web-ui/src/actions/projects.js52
-rw-r--r--opendc-web/opendc-web-ui/src/actions/scenarios.js43
-rw-r--r--opendc-web/opendc-web-ui/src/actions/topologies.js17
-rw-r--r--opendc-web/opendc-web-ui/src/actions/topology/building.js105
-rw-r--r--opendc-web/opendc-web-ui/src/actions/topology/machine.js25
-rw-r--r--opendc-web/opendc-web-ui/src/actions/topology/rack.js23
-rw-r--r--opendc-web/opendc-web-ui/src/actions/topology/room.js48
-rw-r--r--opendc-web/opendc-web-ui/src/actions/users.js37
-rw-r--r--opendc-web/opendc-web-ui/src/api/index.js13
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/portfolios.js42
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/prefabs.js40
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/projects.js40
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/scenarios.js42
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/schedulers.js5
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/token-signin.js10
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/topologies.js42
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/traces.js5
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/users.js48
-rw-r--r--opendc-web/opendc-web-ui/src/api/routes/util.js37
-rw-r--r--opendc-web/opendc-web-ui/src/api/socket.js50
-rw-r--r--opendc-web/opendc-web-ui/src/auth/index.js57
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js11
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js28
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js103
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js13
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js11
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.sass9
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js13
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.sass5
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js24
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/Backdrop.js8
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/GrayLayer.js17
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js27
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/ImageComponent.js48
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js68
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/RoomTile.js20
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/TileObject.js25
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js44
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/elements/WallSegment.js32
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/GridGroup.js34
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js25
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js48
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js35
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js44
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/WallGroup.js22
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js75
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js17
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js11
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js6
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js93
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js53
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.sass50
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js66
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js15
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js62
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js60
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js13
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js31
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js13
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js26
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js10
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js10
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineNameComponent.js5
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js18
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js35
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js52
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js20
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js78
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js10
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js10
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackComponent.js10
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js13
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js43
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js20
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.sass2
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameComponent.js6
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js25
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass11
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js10
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js10
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomComponent.js22
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js27
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomNameComponent.js6
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js20
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ContactSection.js54
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ContactSection.sass15
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ContentSection.js19
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ContentSection.sass9
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/IntroSection.js40
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.js18
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.sass24
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ModelingSection.js22
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.js24
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.sass5
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/SimulationSection.js22
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js30
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/TeamSection.js53
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/TechnologiesSection.js40
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js37
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/Modal.js53
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js54
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js78
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js144
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js71
-rw-r--r--opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js26
-rw-r--r--opendc-web/opendc-web-ui/src/components/navigation/HomeNavbar.js23
-rw-r--r--opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js17
-rw-r--r--opendc-web/opendc-web-ui/src/components/navigation/Navbar.js92
-rw-r--r--opendc-web/opendc-web-ui/src/components/navigation/Navbar.sass30
-rw-r--r--opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js6
-rw-r--r--opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.sass35
-rw-r--r--opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js28
-rw-r--r--opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.sass3
-rw-r--r--opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js33
-rw-r--r--opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.sass70
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/FilterButton.js24
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js13
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/FilterPanel.sass5
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewProjectButtonComponent.js17
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js29
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectAuthList.js39
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectAuthRow.js24
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js22
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js26
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js14
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js21
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js26
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js17
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js19
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js33
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js46
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js28
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js45
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js10
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js41
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js46
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js5
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js25
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js15
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js19
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js20
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js17
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js5
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/EmptySlotContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js19
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js19
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js21
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js21
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js19
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/auth/Login.js62
-rw-r--r--opendc-web/opendc-web-ui/src/containers/auth/Logout.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js14
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/DeleteMachineModal.js35
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js35
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/DeleteRackModal.js35
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/DeleteRoomModal.js35
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/EditRackNameModal.js40
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/EditRoomNameModal.js38
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/NewPortfolioModal.js30
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/NewProjectModal.js30
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js50
-rw-r--r--opendc-web/opendc-web-ui/src/containers/modals/NewTopologyModal.js42
-rw-r--r--opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/projects/FilterLink.js19
-rw-r--r--opendc-web/opendc-web-ui/src/containers/projects/NewProjectButtonContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js20
-rw-r--r--opendc-web/opendc-web-ui/src/containers/projects/VisibleProjectAuthList.js32
-rw-r--r--opendc-web/opendc-web-ui/src/index.js30
-rw-r--r--opendc-web/opendc-web-ui/src/index.sass52
-rw-r--r--opendc-web/opendc-web-ui/src/pages/App.js137
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Home.js33
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Home.sass9
-rw-r--r--opendc-web/opendc-web-ui/src/pages/NotFound.js14
-rw-r--r--opendc-web/opendc-web-ui/src/pages/NotFound.sass11
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Profile.js35
-rw-r--r--opendc-web/opendc-web-ui/src/pages/Projects.js43
-rw-r--r--opendc-web/opendc-web-ui/src/reducers/auth.js12
-rw-r--r--opendc-web/opendc-web-ui/src/reducers/construction-mode.js52
-rw-r--r--opendc-web/opendc-web-ui/src/reducers/current-ids.js54
-rw-r--r--opendc-web/opendc-web-ui/src/reducers/index.js25
-rw-r--r--opendc-web/opendc-web-ui/src/reducers/interaction-level.js61
-rw-r--r--opendc-web/opendc-web-ui/src/reducers/map.js35
-rw-r--r--opendc-web/opendc-web-ui/src/reducers/modals.js45
-rw-r--r--opendc-web/opendc-web-ui/src/reducers/objects.js64
-rw-r--r--opendc-web/opendc-web-ui/src/reducers/project-list.js30
-rw-r--r--opendc-web/opendc-web-ui/src/routes/index.js40
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/index.js80
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/objects.js229
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/portfolios.js131
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/prefabs.js15
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/profile.js12
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/projects.js48
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/scenarios.js65
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/topology.js311
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/users.js44
-rw-r--r--opendc-web/opendc-web-ui/src/shapes/index.js148
-rw-r--r--opendc-web/opendc-web-ui/src/shortcuts/keymap.js10
-rw-r--r--opendc-web/opendc-web-ui/src/store/configure-store.js35
-rw-r--r--opendc-web/opendc-web-ui/src/store/middlewares/dummy-middleware.js3
-rw-r--r--opendc-web/opendc-web-ui/src/store/middlewares/viewport-adjustment.js73
-rw-r--r--opendc-web/opendc-web-ui/src/style-globals/_mixins.sass21
-rw-r--r--opendc-web/opendc-web-ui/src/style-globals/_variables.sass31
-rw-r--r--opendc-web/opendc-web-ui/src/util/authorizations.js11
-rw-r--r--opendc-web/opendc-web-ui/src/util/available-metrics.js67
-rw-r--r--opendc-web/opendc-web-ui/src/util/colors.js29
-rw-r--r--opendc-web/opendc-web-ui/src/util/date-time.js93
-rw-r--r--opendc-web/opendc-web-ui/src/util/date-time.test.js35
-rw-r--r--opendc-web/opendc-web-ui/src/util/sidebar-space.js2
-rw-r--r--opendc-web/opendc-web-ui/src/util/state-utils.js6
-rw-r--r--opendc-web/opendc-web-ui/src/util/tile-calculations.js255
-rw-r--r--opendc-web/opendc-web-ui/src/util/timeline.js9
-rw-r--r--opendc-web/opendc-web-ui/src/util/unit-specifications.js102
241 files changed, 8335 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-ui/src/actions/auth.js b/opendc-web/opendc-web-ui/src/actions/auth.js
new file mode 100644
index 00000000..38c1a782
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/auth.js
@@ -0,0 +1,23 @@
+export const LOG_IN = 'LOG_IN'
+export const LOG_IN_SUCCEEDED = 'LOG_IN_SUCCEEDED'
+export const LOG_OUT = 'LOG_OUT'
+
+export function logIn(payload) {
+ return {
+ type: LOG_IN,
+ payload,
+ }
+}
+
+export function logInSucceeded(payload) {
+ return {
+ type: LOG_IN_SUCCEEDED,
+ payload,
+ }
+}
+
+export function logOut() {
+ return {
+ type: LOG_OUT,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/interaction-level.js b/opendc-web/opendc-web-ui/src/actions/interaction-level.js
new file mode 100644
index 00000000..ff6b1fa3
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/interaction-level.js
@@ -0,0 +1,50 @@
+export const GO_FROM_BUILDING_TO_ROOM = 'GO_FROM_BUILDING_TO_ROOM'
+export const GO_FROM_ROOM_TO_RACK = 'GO_FROM_ROOM_TO_RACK'
+export const GO_FROM_RACK_TO_MACHINE = 'GO_FROM_RACK_TO_MACHINE'
+export const GO_DOWN_ONE_INTERACTION_LEVEL = 'GO_DOWN_ONE_INTERACTION_LEVEL'
+
+export function goFromBuildingToRoom(roomId) {
+ return (dispatch, getState) => {
+ const { interactionLevel } = getState()
+ if (interactionLevel.mode !== 'BUILDING') {
+ return
+ }
+
+ dispatch({
+ type: GO_FROM_BUILDING_TO_ROOM,
+ roomId,
+ })
+ }
+}
+
+export function goFromRoomToRack(tileId) {
+ return (dispatch, getState) => {
+ const { interactionLevel } = getState()
+ if (interactionLevel.mode !== 'ROOM') {
+ return
+ }
+ dispatch({
+ type: GO_FROM_ROOM_TO_RACK,
+ tileId,
+ })
+ }
+}
+
+export function goFromRackToMachine(position) {
+ return (dispatch, getState) => {
+ const { interactionLevel } = getState()
+ if (interactionLevel.mode !== 'RACK') {
+ return
+ }
+ dispatch({
+ type: GO_FROM_RACK_TO_MACHINE,
+ position,
+ })
+ }
+}
+
+export function goDownOneInteractionLevel() {
+ return {
+ type: GO_DOWN_ONE_INTERACTION_LEVEL,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/map.js b/opendc-web/opendc-web-ui/src/actions/map.js
new file mode 100644
index 00000000..0d49d849
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/map.js
@@ -0,0 +1,82 @@
+import {
+ MAP_MAX_SCALE,
+ MAP_MIN_SCALE,
+ MAP_SCALE_PER_EVENT,
+ MAP_SIZE_IN_PIXELS,
+} from '../components/app/map/MapConstants'
+
+export const SET_MAP_POSITION = 'SET_MAP_POSITION'
+export const SET_MAP_DIMENSIONS = 'SET_MAP_DIMENSIONS'
+export const SET_MAP_SCALE = 'SET_MAP_SCALE'
+
+export function setMapPosition(x, y) {
+ return {
+ type: SET_MAP_POSITION,
+ x,
+ y,
+ }
+}
+
+export function setMapDimensions(width, height) {
+ return {
+ type: SET_MAP_DIMENSIONS,
+ width,
+ height,
+ }
+}
+
+export function setMapScale(scale) {
+ return {
+ type: SET_MAP_SCALE,
+ scale,
+ }
+}
+
+export function zoomInOnCenter(zoomIn) {
+ return (dispatch, getState) => {
+ const state = getState()
+
+ dispatch(zoomInOnPosition(zoomIn, state.map.dimensions.width / 2, state.map.dimensions.height / 2))
+ }
+}
+
+export function zoomInOnPosition(zoomIn, x, y) {
+ return (dispatch, getState) => {
+ const state = getState()
+
+ const centerPoint = {
+ x: x / state.map.scale - state.map.position.x / state.map.scale,
+ y: y / state.map.scale - state.map.position.y / state.map.scale,
+ }
+ const newScale = zoomIn ? state.map.scale * MAP_SCALE_PER_EVENT : state.map.scale / MAP_SCALE_PER_EVENT
+ const boundedScale = Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE)
+
+ const newX = -(centerPoint.x - x / boundedScale) * boundedScale
+ const newY = -(centerPoint.y - y / boundedScale) * boundedScale
+
+ dispatch(setMapPositionWithBoundsCheck(newX, newY))
+ dispatch(setMapScale(boundedScale))
+ }
+}
+
+export function setMapPositionWithBoundsCheck(x, y) {
+ return (dispatch, getState) => {
+ const state = getState()
+
+ const scaledMapSize = MAP_SIZE_IN_PIXELS * state.map.scale
+ const updatedX =
+ x > 0
+ ? 0
+ : x < -scaledMapSize + state.map.dimensions.width
+ ? -scaledMapSize + state.map.dimensions.width
+ : x
+ const updatedY =
+ y > 0
+ ? 0
+ : y < -scaledMapSize + state.map.dimensions.height
+ ? -scaledMapSize + state.map.dimensions.height
+ : y
+
+ dispatch(setMapPosition(updatedX, updatedY))
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/modals/portfolios.js b/opendc-web/opendc-web-ui/src/actions/modals/portfolios.js
new file mode 100644
index 00000000..f6dce2e3
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/modals/portfolios.js
@@ -0,0 +1,14 @@
+export const OPEN_NEW_PORTFOLIO_MODAL = 'OPEN_NEW_PORTFOLIO_MODAL'
+export const CLOSE_NEW_PORTFOLIO_MODAL = 'CLOSE_PORTFOLIO_MODAL'
+
+export function openNewPortfolioModal() {
+ return {
+ type: OPEN_NEW_PORTFOLIO_MODAL,
+ }
+}
+
+export function closeNewPortfolioModal() {
+ return {
+ type: CLOSE_NEW_PORTFOLIO_MODAL,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/modals/prefabs.js b/opendc-web/opendc-web-ui/src/actions/modals/prefabs.js
new file mode 100644
index 00000000..826565d2
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/modals/prefabs.js
@@ -0,0 +1,14 @@
+export const OPEN_NEW_PREFAB_MODAL = 'OPEN_NEW_PREFAB_MODAL'
+export const CLOSE_NEW_PREFAB_MODAL = 'CLOSE_PREFAB_MODAL'
+
+export function openNewPrefabModal() {
+ return {
+ type: OPEN_NEW_PREFAB_MODAL,
+ }
+}
+
+export function closeNewPrefabModal() {
+ return {
+ type: CLOSE_NEW_PREFAB_MODAL,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/modals/profile.js b/opendc-web/opendc-web-ui/src/actions/modals/profile.js
new file mode 100644
index 00000000..39c72c03
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/modals/profile.js
@@ -0,0 +1,14 @@
+export const OPEN_DELETE_PROFILE_MODAL = 'OPEN_DELETE_PROFILE_MODAL'
+export const CLOSE_DELETE_PROFILE_MODAL = 'CLOSE_DELETE_PROFILE_MODAL'
+
+export function openDeleteProfileModal() {
+ return {
+ type: OPEN_DELETE_PROFILE_MODAL,
+ }
+}
+
+export function closeDeleteProfileModal() {
+ return {
+ type: CLOSE_DELETE_PROFILE_MODAL,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/modals/projects.js b/opendc-web/opendc-web-ui/src/actions/modals/projects.js
new file mode 100644
index 00000000..d1043cbb
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/modals/projects.js
@@ -0,0 +1,14 @@
+export const OPEN_NEW_PROJECT_MODAL = 'OPEN_NEW_PROJECT_MODAL'
+export const CLOSE_NEW_PROJECT_MODAL = 'CLOSE_PROJECT_MODAL'
+
+export function openNewProjectModal() {
+ return {
+ type: OPEN_NEW_PROJECT_MODAL,
+ }
+}
+
+export function closeNewProjectModal() {
+ return {
+ type: CLOSE_NEW_PROJECT_MODAL,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/modals/scenarios.js b/opendc-web/opendc-web-ui/src/actions/modals/scenarios.js
new file mode 100644
index 00000000..b71cb27b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/modals/scenarios.js
@@ -0,0 +1,14 @@
+export const OPEN_NEW_SCENARIO_MODAL = 'OPEN_NEW_SCENARIO_MODAL'
+export const CLOSE_NEW_SCENARIO_MODAL = 'CLOSE_SCENARIO_MODAL'
+
+export function openNewScenarioModal() {
+ return {
+ type: OPEN_NEW_SCENARIO_MODAL,
+ }
+}
+
+export function closeNewScenarioModal() {
+ return {
+ type: CLOSE_NEW_SCENARIO_MODAL,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/modals/topology.js b/opendc-web/opendc-web-ui/src/actions/modals/topology.js
new file mode 100644
index 00000000..b5fecac1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/modals/topology.js
@@ -0,0 +1,84 @@
+export const OPEN_NEW_TOPOLOGY_MODAL = 'OPEN_NEW_TOPOLOGY_MODAL'
+export const CLOSE_NEW_TOPOLOGY_MODAL = 'CLOSE_NEW_TOPOLOGY_MODAL'
+export const OPEN_EDIT_ROOM_NAME_MODAL = 'OPEN_EDIT_ROOM_NAME_MODAL'
+export const CLOSE_EDIT_ROOM_NAME_MODAL = 'CLOSE_EDIT_ROOM_NAME_MODAL'
+export const OPEN_DELETE_ROOM_MODAL = 'OPEN_DELETE_ROOM_MODAL'
+export const CLOSE_DELETE_ROOM_MODAL = 'CLOSE_DELETE_ROOM_MODAL'
+export const OPEN_EDIT_RACK_NAME_MODAL = 'OPEN_EDIT_RACK_NAME_MODAL'
+export const CLOSE_EDIT_RACK_NAME_MODAL = 'CLOSE_EDIT_RACK_NAME_MODAL'
+export const OPEN_DELETE_RACK_MODAL = 'OPEN_DELETE_RACK_MODAL'
+export const CLOSE_DELETE_RACK_MODAL = 'CLOSE_DELETE_RACK_MODAL'
+export const OPEN_DELETE_MACHINE_MODAL = 'OPEN_DELETE_MACHINE_MODAL'
+export const CLOSE_DELETE_MACHINE_MODAL = 'CLOSE_DELETE_MACHINE_MODAL'
+
+export function openNewTopologyModal() {
+ return {
+ type: OPEN_NEW_TOPOLOGY_MODAL,
+ }
+}
+
+export function closeNewTopologyModal() {
+ return {
+ type: CLOSE_NEW_TOPOLOGY_MODAL,
+ }
+}
+
+export function openEditRoomNameModal() {
+ return {
+ type: OPEN_EDIT_ROOM_NAME_MODAL,
+ }
+}
+
+export function closeEditRoomNameModal() {
+ return {
+ type: CLOSE_EDIT_ROOM_NAME_MODAL,
+ }
+}
+
+export function openDeleteRoomModal() {
+ return {
+ type: OPEN_DELETE_ROOM_MODAL,
+ }
+}
+
+export function closeDeleteRoomModal() {
+ return {
+ type: CLOSE_DELETE_ROOM_MODAL,
+ }
+}
+
+export function openEditRackNameModal() {
+ return {
+ type: OPEN_EDIT_RACK_NAME_MODAL,
+ }
+}
+
+export function closeEditRackNameModal() {
+ return {
+ type: CLOSE_EDIT_RACK_NAME_MODAL,
+ }
+}
+
+export function openDeleteRackModal() {
+ return {
+ type: OPEN_DELETE_RACK_MODAL,
+ }
+}
+
+export function closeDeleteRackModal() {
+ return {
+ type: CLOSE_DELETE_RACK_MODAL,
+ }
+}
+
+export function openDeleteMachineModal() {
+ return {
+ type: OPEN_DELETE_MACHINE_MODAL,
+ }
+}
+
+export function closeDeleteMachineModal() {
+ return {
+ type: CLOSE_DELETE_MACHINE_MODAL,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/objects.js b/opendc-web/opendc-web-ui/src/actions/objects.js
new file mode 100644
index 00000000..7b648b18
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/objects.js
@@ -0,0 +1,41 @@
+export const ADD_TO_STORE = 'ADD_TO_STORE'
+export const ADD_PROP_TO_STORE_OBJECT = 'ADD_PROP_TO_STORE_OBJECT'
+export const ADD_ID_TO_STORE_OBJECT_LIST_PROP = 'ADD_ID_TO_STORE_OBJECT_LIST_PROP'
+export const REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP = 'REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP'
+
+export function addToStore(objectType, object) {
+ return {
+ type: ADD_TO_STORE,
+ objectType,
+ object,
+ }
+}
+
+export function addPropToStoreObject(objectType, objectId, propObject) {
+ return {
+ type: ADD_PROP_TO_STORE_OBJECT,
+ objectType,
+ objectId,
+ propObject,
+ }
+}
+
+export function addIdToStoreObjectListProp(objectType, objectId, propName, id) {
+ return {
+ type: ADD_ID_TO_STORE_OBJECT_LIST_PROP,
+ objectType,
+ objectId,
+ propName,
+ id,
+ }
+}
+
+export function removeIdFromStoreObjectListProp(objectType, objectId, propName, id) {
+ return {
+ type: REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP,
+ objectType,
+ objectId,
+ propName,
+ id,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/portfolios.js b/opendc-web/opendc-web-ui/src/actions/portfolios.js
new file mode 100644
index 00000000..d37886d8
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/portfolios.js
@@ -0,0 +1,41 @@
+export const ADD_PORTFOLIO = 'ADD_PORTFOLIO'
+export const UPDATE_PORTFOLIO = 'UPDATE_PORTFOLIO'
+export const DELETE_PORTFOLIO = 'DELETE_PORTFOLIO'
+export const OPEN_PORTFOLIO_SUCCEEDED = 'OPEN_PORTFOLIO_SUCCEEDED'
+export const SET_CURRENT_PORTFOLIO = 'SET_CURRENT_PORTFOLIO'
+
+export function addPortfolio(portfolio) {
+ return {
+ type: ADD_PORTFOLIO,
+ portfolio,
+ }
+}
+
+export function updatePortfolio(portfolio) {
+ return {
+ type: UPDATE_PORTFOLIO,
+ portfolio,
+ }
+}
+
+export function deletePortfolio(id) {
+ return {
+ type: DELETE_PORTFOLIO,
+ id,
+ }
+}
+
+export function openPortfolioSucceeded(projectId, portfolioId) {
+ return {
+ type: OPEN_PORTFOLIO_SUCCEEDED,
+ projectId,
+ portfolioId,
+ }
+}
+
+export function setCurrentPortfolio(portfolioId) {
+ return {
+ type: SET_CURRENT_PORTFOLIO,
+ portfolioId,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/prefabs.js b/opendc-web/opendc-web-ui/src/actions/prefabs.js
new file mode 100644
index 00000000..c112feed
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/prefabs.js
@@ -0,0 +1,32 @@
+export const ADD_PREFAB = 'ADD_PREFAB'
+export const DELETE_PREFAB = 'DELETE_PREFAB'
+export const DELETE_PREFAB_SUCCEEDED = 'DELETE_PREFAB_SUCCEEDED'
+export const OPEN_PREFAB_SUCCEEDED = 'OPEN_PREFAB_SUCCEEDED'
+
+export function addPrefab(name) {
+ return {
+ type: ADD_PREFAB,
+ name,
+ }
+}
+
+export function deletePrefab(id) {
+ return {
+ type: DELETE_PREFAB,
+ id,
+ }
+}
+
+export function deletePrefabSucceeded(id) {
+ return {
+ type: DELETE_PREFAB_SUCCEEDED,
+ id,
+ }
+}
+
+export function openPrefabSucceeded(id) {
+ return {
+ type: OPEN_PREFAB_SUCCEEDED,
+ id,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/projects.js b/opendc-web/opendc-web-ui/src/actions/projects.js
new file mode 100644
index 00000000..add0f242
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/projects.js
@@ -0,0 +1,52 @@
+export const SET_AUTH_VISIBILITY_FILTER = 'SET_AUTH_VISIBILITY_FILTER'
+export const ADD_PROJECT = 'ADD_PROJECT'
+export const ADD_PROJECT_SUCCEEDED = 'ADD_PROJECT_SUCCEEDED'
+export const DELETE_PROJECT = 'DELETE_PROJECT'
+export const DELETE_PROJECT_SUCCEEDED = 'DELETE_PROJECT_SUCCEEDED'
+export const OPEN_PROJECT_SUCCEEDED = 'OPEN_PROJECT_SUCCEEDED'
+
+export function setAuthVisibilityFilter(filter) {
+ return {
+ type: SET_AUTH_VISIBILITY_FILTER,
+ filter,
+ }
+}
+
+export function addProject(name) {
+ return (dispatch, getState) => {
+ const { auth } = getState()
+ dispatch({
+ type: ADD_PROJECT,
+ name,
+ userId: auth.userId,
+ })
+ }
+}
+
+export function addProjectSucceeded(authorization) {
+ return {
+ type: ADD_PROJECT_SUCCEEDED,
+ authorization,
+ }
+}
+
+export function deleteProject(id) {
+ return {
+ type: DELETE_PROJECT,
+ id,
+ }
+}
+
+export function deleteProjectSucceeded(id) {
+ return {
+ type: DELETE_PROJECT_SUCCEEDED,
+ id,
+ }
+}
+
+export function openProjectSucceeded(id) {
+ return {
+ type: OPEN_PROJECT_SUCCEEDED,
+ id,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/scenarios.js b/opendc-web/opendc-web-ui/src/actions/scenarios.js
new file mode 100644
index 00000000..c8a90762
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/scenarios.js
@@ -0,0 +1,43 @@
+export const ADD_SCENARIO = 'ADD_SCENARIO'
+export const UPDATE_SCENARIO = 'UPDATE_SCENARIO'
+export const DELETE_SCENARIO = 'DELETE_SCENARIO'
+export const OPEN_SCENARIO_SUCCEEDED = 'OPEN_SCENARIO_SUCCEEDED'
+export const SET_CURRENT_SCENARIO = 'SET_CURRENT_SCENARIO'
+
+export function addScenario(scenario) {
+ return {
+ type: ADD_SCENARIO,
+ scenario,
+ }
+}
+
+export function updateScenario(scenario) {
+ return {
+ type: UPDATE_SCENARIO,
+ scenario,
+ }
+}
+
+export function deleteScenario(id) {
+ return {
+ type: DELETE_SCENARIO,
+ id,
+ }
+}
+
+export function openScenarioSucceeded(projectId, portfolioId, scenarioId) {
+ return {
+ type: OPEN_SCENARIO_SUCCEEDED,
+ projectId,
+ portfolioId,
+ scenarioId,
+ }
+}
+
+export function setCurrentScenario(portfolioId, scenarioId) {
+ return {
+ type: SET_CURRENT_SCENARIO,
+ portfolioId,
+ scenarioId,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/topologies.js b/opendc-web/opendc-web-ui/src/actions/topologies.js
new file mode 100644
index 00000000..dcce3b7d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/topologies.js
@@ -0,0 +1,17 @@
+export const ADD_TOPOLOGY = 'ADD_TOPOLOGY'
+export const DELETE_TOPOLOGY = 'DELETE_TOPOLOGY'
+
+export function addTopology(name, duplicateId) {
+ return {
+ type: ADD_TOPOLOGY,
+ name,
+ duplicateId,
+ }
+}
+
+export function deleteTopology(id) {
+ return {
+ type: DELETE_TOPOLOGY,
+ id,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/topology/building.js b/opendc-web/opendc-web-ui/src/actions/topology/building.js
new file mode 100644
index 00000000..72deda6f
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/topology/building.js
@@ -0,0 +1,105 @@
+export const SET_CURRENT_TOPOLOGY = 'SET_CURRENT_TOPOLOGY'
+export const START_NEW_ROOM_CONSTRUCTION = 'START_NEW_ROOM_CONSTRUCTION'
+export const START_NEW_ROOM_CONSTRUCTION_SUCCEEDED = 'START_NEW_ROOM_CONSTRUCTION_SUCCEEDED'
+export const FINISH_NEW_ROOM_CONSTRUCTION = 'FINISH_NEW_ROOM_CONSTRUCTION'
+export const CANCEL_NEW_ROOM_CONSTRUCTION = 'CANCEL_NEW_ROOM_CONSTRUCTION'
+export const CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED = 'CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED'
+export const START_ROOM_EDIT = 'START_ROOM_EDIT'
+export const FINISH_ROOM_EDIT = 'FINISH_ROOM_EDIT'
+export const ADD_TILE = 'ADD_TILE'
+export const DELETE_TILE = 'DELETE_TILE'
+
+export function setCurrentTopology(topologyId) {
+ return {
+ type: SET_CURRENT_TOPOLOGY,
+ topologyId,
+ }
+}
+
+export function startNewRoomConstruction() {
+ return {
+ type: START_NEW_ROOM_CONSTRUCTION,
+ }
+}
+
+export function startNewRoomConstructionSucceeded(roomId) {
+ return {
+ type: START_NEW_ROOM_CONSTRUCTION_SUCCEEDED,
+ roomId,
+ }
+}
+
+export function finishNewRoomConstruction() {
+ return (dispatch, getState) => {
+ const { objects, construction } = getState()
+ if (objects.room[construction.currentRoomInConstruction].tileIds.length === 0) {
+ dispatch(cancelNewRoomConstruction())
+ return
+ }
+
+ dispatch({
+ type: FINISH_NEW_ROOM_CONSTRUCTION,
+ })
+ }
+}
+
+export function cancelNewRoomConstruction() {
+ return {
+ type: CANCEL_NEW_ROOM_CONSTRUCTION,
+ }
+}
+
+export function cancelNewRoomConstructionSucceeded() {
+ return {
+ type: CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED,
+ }
+}
+
+export function startRoomEdit() {
+ return (dispatch, getState) => {
+ const { interactionLevel } = getState()
+ dispatch({
+ type: START_ROOM_EDIT,
+ roomId: interactionLevel.roomId,
+ })
+ }
+}
+
+export function finishRoomEdit() {
+ return {
+ type: FINISH_ROOM_EDIT,
+ }
+}
+
+export function toggleTileAtLocation(positionX, positionY) {
+ return (dispatch, getState) => {
+ const { objects, construction } = getState()
+
+ const tileIds = objects.room[construction.currentRoomInConstruction].tileIds
+ for (let index in tileIds) {
+ if (
+ objects.tile[tileIds[index]].positionX === positionX &&
+ objects.tile[tileIds[index]].positionY === positionY
+ ) {
+ dispatch(deleteTile(tileIds[index]))
+ return
+ }
+ }
+ dispatch(addTile(positionX, positionY))
+ }
+}
+
+export function addTile(positionX, positionY) {
+ return {
+ type: ADD_TILE,
+ positionX,
+ positionY,
+ }
+}
+
+export function deleteTile(tileId) {
+ return {
+ type: DELETE_TILE,
+ tileId,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/topology/machine.js b/opendc-web/opendc-web-ui/src/actions/topology/machine.js
new file mode 100644
index 00000000..17ccce5d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/topology/machine.js
@@ -0,0 +1,25 @@
+export const DELETE_MACHINE = 'DELETE_MACHINE'
+export const ADD_UNIT = 'ADD_UNIT'
+export const DELETE_UNIT = 'DELETE_UNIT'
+
+export function deleteMachine() {
+ return {
+ type: DELETE_MACHINE,
+ }
+}
+
+export function addUnit(unitType, id) {
+ return {
+ type: ADD_UNIT,
+ unitType,
+ id,
+ }
+}
+
+export function deleteUnit(unitType, index) {
+ return {
+ type: DELETE_UNIT,
+ unitType,
+ index,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/topology/rack.js b/opendc-web/opendc-web-ui/src/actions/topology/rack.js
new file mode 100644
index 00000000..b117402e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/topology/rack.js
@@ -0,0 +1,23 @@
+export const EDIT_RACK_NAME = 'EDIT_RACK_NAME'
+export const DELETE_RACK = 'DELETE_RACK'
+export const ADD_MACHINE = 'ADD_MACHINE'
+
+export function editRackName(name) {
+ return {
+ type: EDIT_RACK_NAME,
+ name,
+ }
+}
+
+export function deleteRack() {
+ return {
+ type: DELETE_RACK,
+ }
+}
+
+export function addMachine(position) {
+ return {
+ type: ADD_MACHINE,
+ position,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/topology/room.js b/opendc-web/opendc-web-ui/src/actions/topology/room.js
new file mode 100644
index 00000000..52cba680
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/topology/room.js
@@ -0,0 +1,48 @@
+import { findTileWithPosition } from '../../util/tile-calculations'
+
+export const EDIT_ROOM_NAME = 'EDIT_ROOM_NAME'
+export const DELETE_ROOM = 'DELETE_ROOM'
+export const START_RACK_CONSTRUCTION = 'START_RACK_CONSTRUCTION'
+export const STOP_RACK_CONSTRUCTION = 'STOP_RACK_CONSTRUCTION'
+export const ADD_RACK_TO_TILE = 'ADD_RACK_TO_TILE'
+
+export function editRoomName(name) {
+ return {
+ type: EDIT_ROOM_NAME,
+ name,
+ }
+}
+
+export function startRackConstruction() {
+ return {
+ type: START_RACK_CONSTRUCTION,
+ }
+}
+
+export function stopRackConstruction() {
+ return {
+ type: STOP_RACK_CONSTRUCTION,
+ }
+}
+
+export function addRackToTile(positionX, positionY) {
+ return (dispatch, getState) => {
+ const { objects, interactionLevel } = getState()
+ const currentRoom = objects.room[interactionLevel.roomId]
+ const tiles = currentRoom.tileIds.map((tileId) => objects.tile[tileId])
+ const tile = findTileWithPosition(tiles, positionX, positionY)
+
+ if (tile !== null) {
+ dispatch({
+ type: ADD_RACK_TO_TILE,
+ tileId: tile._id,
+ })
+ }
+ }
+}
+
+export function deleteRoom() {
+ return {
+ type: DELETE_ROOM,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/actions/users.js b/opendc-web/opendc-web-ui/src/actions/users.js
new file mode 100644
index 00000000..4868ac34
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/actions/users.js
@@ -0,0 +1,37 @@
+export const FETCH_AUTHORIZATIONS_OF_CURRENT_USER = 'FETCH_AUTHORIZATIONS_OF_CURRENT_USER'
+export const FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED = 'FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED'
+export const DELETE_CURRENT_USER = 'DELETE_CURRENT_USER'
+export const DELETE_CURRENT_USER_SUCCEEDED = 'DELETE_CURRENT_USER_SUCCEEDED'
+
+export function fetchAuthorizationsOfCurrentUser() {
+ return (dispatch, getState) => {
+ const { auth } = getState()
+ dispatch({
+ type: FETCH_AUTHORIZATIONS_OF_CURRENT_USER,
+ userId: auth.userId,
+ })
+ }
+}
+
+export function fetchAuthorizationsOfCurrentUserSucceeded(authorizationsOfCurrentUser) {
+ return {
+ type: FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED,
+ authorizationsOfCurrentUser,
+ }
+}
+
+export function deleteCurrentUser() {
+ return (dispatch, getState) => {
+ const { auth } = getState()
+ dispatch({
+ type: DELETE_CURRENT_USER,
+ userId: auth.userId,
+ })
+ }
+}
+
+export function deleteCurrentUserSucceeded() {
+ return {
+ type: DELETE_CURRENT_USER_SUCCEEDED,
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/api/index.js b/opendc-web/opendc-web-ui/src/api/index.js
new file mode 100644
index 00000000..cefcb2c5
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/index.js
@@ -0,0 +1,13 @@
+import { sendSocketRequest } from './socket'
+
+export function sendRequest(request) {
+ return new Promise((resolve, reject) => {
+ sendSocketRequest(request, (response) => {
+ if (response.status.code === 200) {
+ resolve(response.content)
+ } else {
+ reject(response)
+ }
+ })
+ })
+}
diff --git a/opendc-web/opendc-web-ui/src/api/routes/portfolios.js b/opendc-web/opendc-web-ui/src/api/routes/portfolios.js
new file mode 100644
index 00000000..7c9ea02a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/routes/portfolios.js
@@ -0,0 +1,42 @@
+import { deleteById, getById } from './util'
+import { sendRequest } from '../index'
+
+export function addPortfolio(projectId, portfolio) {
+ return sendRequest({
+ path: '/projects/{projectId}/portfolios',
+ method: 'POST',
+ parameters: {
+ body: {
+ portfolio,
+ },
+ path: {
+ projectId,
+ },
+ query: {},
+ },
+ })
+}
+
+export function getPortfolio(portfolioId) {
+ return getById('/portfolios/{portfolioId}', { portfolioId })
+}
+
+export function updatePortfolio(portfolioId, portfolio) {
+ return sendRequest({
+ path: '/portfolios/{projectId}',
+ method: 'POST',
+ parameters: {
+ body: {
+ portfolio,
+ },
+ path: {
+ portfolioId,
+ },
+ query: {},
+ },
+ })
+}
+
+export function deletePortfolio(portfolioId) {
+ return deleteById('/portfolios/{portfolioId}', { portfolioId })
+}
diff --git a/opendc-web/opendc-web-ui/src/api/routes/prefabs.js b/opendc-web/opendc-web-ui/src/api/routes/prefabs.js
new file mode 100644
index 00000000..8a1debfa
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/routes/prefabs.js
@@ -0,0 +1,40 @@
+import { sendRequest } from '../index'
+import { deleteById, getById } from './util'
+
+export function getPrefab(prefabId) {
+ return getById('/prefabs/{prefabId}', { prefabId })
+}
+
+export function addPrefab(prefab) {
+ return sendRequest({
+ path: '/prefabs',
+ method: 'POST',
+ parameters: {
+ body: {
+ prefab,
+ },
+ path: {},
+ query: {},
+ },
+ })
+}
+
+export function updatePrefab(prefab) {
+ return sendRequest({
+ path: '/prefabs/{prefabId}',
+ method: 'PUT',
+ parameters: {
+ body: {
+ prefab,
+ },
+ path: {
+ prefabId: prefab._id,
+ },
+ query: {},
+ },
+ })
+}
+
+export function deletePrefab(prefabId) {
+ return deleteById('/prefabs/{prefabId}', { prefabId })
+}
diff --git a/opendc-web/opendc-web-ui/src/api/routes/projects.js b/opendc-web/opendc-web-ui/src/api/routes/projects.js
new file mode 100644
index 00000000..4109079c
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/routes/projects.js
@@ -0,0 +1,40 @@
+import { sendRequest } from '../index'
+import { deleteById, getById } from './util'
+
+export function getProject(projectId) {
+ return getById('/projects/{projectId}', { projectId })
+}
+
+export function addProject(project) {
+ return sendRequest({
+ path: '/projects',
+ method: 'POST',
+ parameters: {
+ body: {
+ project,
+ },
+ path: {},
+ query: {},
+ },
+ })
+}
+
+export function updateProject(project) {
+ return sendRequest({
+ path: '/projects/{projectId}',
+ method: 'PUT',
+ parameters: {
+ body: {
+ project,
+ },
+ path: {
+ projectId: project._id,
+ },
+ query: {},
+ },
+ })
+}
+
+export function deleteProject(projectId) {
+ return deleteById('/projects/{projectId}', { projectId })
+}
diff --git a/opendc-web/opendc-web-ui/src/api/routes/scenarios.js b/opendc-web/opendc-web-ui/src/api/routes/scenarios.js
new file mode 100644
index 00000000..ab2e8b86
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/routes/scenarios.js
@@ -0,0 +1,42 @@
+import { deleteById, getById } from './util'
+import { sendRequest } from '../index'
+
+export function addScenario(portfolioId, scenario) {
+ return sendRequest({
+ path: '/portfolios/{portfolioId}/scenarios',
+ method: 'POST',
+ parameters: {
+ body: {
+ scenario,
+ },
+ path: {
+ portfolioId,
+ },
+ query: {},
+ },
+ })
+}
+
+export function getScenario(scenarioId) {
+ return getById('/scenarios/{scenarioId}', { scenarioId })
+}
+
+export function updateScenario(scenarioId, scenario) {
+ return sendRequest({
+ path: '/scenarios/{projectId}',
+ method: 'POST',
+ parameters: {
+ body: {
+ scenario,
+ },
+ path: {
+ scenarioId,
+ },
+ query: {},
+ },
+ })
+}
+
+export function deleteScenario(scenarioId) {
+ return deleteById('/scenarios/{scenarioId}', { scenarioId })
+}
diff --git a/opendc-web/opendc-web-ui/src/api/routes/schedulers.js b/opendc-web/opendc-web-ui/src/api/routes/schedulers.js
new file mode 100644
index 00000000..4481fb2a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/routes/schedulers.js
@@ -0,0 +1,5 @@
+import { getAll } from './util'
+
+export function getAllSchedulers() {
+ return getAll('/schedulers')
+}
diff --git a/opendc-web/opendc-web-ui/src/api/routes/token-signin.js b/opendc-web/opendc-web-ui/src/api/routes/token-signin.js
new file mode 100644
index 00000000..d6cff570
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/routes/token-signin.js
@@ -0,0 +1,10 @@
+export function performTokenSignIn(token) {
+ const apiUrl = process.env.REACT_APP_API_BASE_URL || ''
+
+ return fetch(`${apiUrl}/tokensignin`, {
+ method: 'POST',
+ body: new URLSearchParams({
+ idtoken: token,
+ }),
+ }).then((res) => res.json())
+}
diff --git a/opendc-web/opendc-web-ui/src/api/routes/topologies.js b/opendc-web/opendc-web-ui/src/api/routes/topologies.js
new file mode 100644
index 00000000..a8f0d6b1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/routes/topologies.js
@@ -0,0 +1,42 @@
+import { deleteById, getById } from './util'
+import { sendRequest } from '../index'
+
+export function addTopology(topology) {
+ return sendRequest({
+ path: '/projects/{projectId}/topologies',
+ method: 'POST',
+ parameters: {
+ body: {
+ topology,
+ },
+ path: {
+ projectId: topology.projectId,
+ },
+ query: {},
+ },
+ })
+}
+
+export function getTopology(topologyId) {
+ return getById('/topologies/{topologyId}', { topologyId })
+}
+
+export function updateTopology(topology) {
+ return sendRequest({
+ path: '/topologies/{topologyId}',
+ method: 'PUT',
+ parameters: {
+ body: {
+ topology,
+ },
+ path: {
+ topologyId: topology._id,
+ },
+ query: {},
+ },
+ })
+}
+
+export function deleteTopology(topologyId) {
+ return deleteById('/topologies/{topologyId}', { topologyId })
+}
diff --git a/opendc-web/opendc-web-ui/src/api/routes/traces.js b/opendc-web/opendc-web-ui/src/api/routes/traces.js
new file mode 100644
index 00000000..67895a87
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/routes/traces.js
@@ -0,0 +1,5 @@
+import { getAll } from './util'
+
+export function getAllTraces() {
+ return getAll('/traces')
+}
diff --git a/opendc-web/opendc-web-ui/src/api/routes/users.js b/opendc-web/opendc-web-ui/src/api/routes/users.js
new file mode 100644
index 00000000..3028f3f7
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/routes/users.js
@@ -0,0 +1,48 @@
+import { sendRequest } from '../index'
+import { deleteById } from './util'
+
+export function getUserByEmail(email) {
+ return sendRequest({
+ path: '/users',
+ method: 'GET',
+ parameters: {
+ body: {},
+ path: {},
+ query: {
+ email,
+ },
+ },
+ })
+}
+
+export function addUser(user) {
+ return sendRequest({
+ path: '/users',
+ method: 'POST',
+ parameters: {
+ body: {
+ user,
+ },
+ path: {},
+ query: {},
+ },
+ })
+}
+
+export function getUser(userId) {
+ return sendRequest({
+ path: '/users/{userId}',
+ method: 'GET',
+ parameters: {
+ body: {},
+ path: {
+ userId,
+ },
+ query: {},
+ },
+ })
+}
+
+export function deleteUser(userId) {
+ return deleteById('/users/{userId}', { userId })
+}
diff --git a/opendc-web/opendc-web-ui/src/api/routes/util.js b/opendc-web/opendc-web-ui/src/api/routes/util.js
new file mode 100644
index 00000000..67e7173b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/routes/util.js
@@ -0,0 +1,37 @@
+import { sendRequest } from '../index'
+
+export function getAll(path) {
+ return sendRequest({
+ path,
+ method: 'GET',
+ parameters: {
+ body: {},
+ path: {},
+ query: {},
+ },
+ })
+}
+
+export function getById(path, pathObject) {
+ return sendRequest({
+ path,
+ method: 'GET',
+ parameters: {
+ body: {},
+ path: pathObject,
+ query: {},
+ },
+ })
+}
+
+export function deleteById(path, pathObject) {
+ return sendRequest({
+ path,
+ method: 'DELETE',
+ parameters: {
+ body: {},
+ path: pathObject,
+ query: {},
+ },
+ })
+}
diff --git a/opendc-web/opendc-web-ui/src/api/socket.js b/opendc-web/opendc-web-ui/src/api/socket.js
new file mode 100644
index 00000000..1c432167
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/socket.js
@@ -0,0 +1,50 @@
+import io from 'socket.io-client'
+import { getAuthToken } from '../auth/index'
+
+let socket
+let requestIdCounter = 0
+const callbacks = {}
+
+export function setupSocketConnection(onConnect) {
+ const apiUrl =
+ process.env.REACT_APP_API_BASE_URL ||
+ `${window.location.protocol}//${window.location.hostname}:${window.location.port}`
+
+ socket = io.connect(apiUrl)
+ socket.on('connect', onConnect)
+ socket.on('response', onSocketResponse)
+}
+
+export function sendSocketRequest(request, callback) {
+ if (!socket.connected) {
+ console.error('Attempted to send request over unconnected socket')
+ return
+ }
+
+ const newId = requestIdCounter++
+ callbacks[newId] = callback
+
+ request.id = newId
+ request.token = getAuthToken()
+
+ if (!request.isRootRoute) {
+ request.path = '/v2' + request.path
+ }
+
+ socket.emit('request', request)
+
+ if (process.env.NODE_ENV !== 'production') {
+ console.log('Sent socket request:', request)
+ }
+}
+
+function onSocketResponse(json) {
+ const response = JSON.parse(json)
+
+ if (process.env.NODE_ENV !== 'production') {
+ console.log('Received socket response:', response)
+ }
+
+ callbacks[response.id](response)
+ delete callbacks[response.id]
+}
diff --git a/opendc-web/opendc-web-ui/src/auth/index.js b/opendc-web/opendc-web-ui/src/auth/index.js
new file mode 100644
index 00000000..b5953990
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/auth/index.js
@@ -0,0 +1,57 @@
+import { LOG_IN_SUCCEEDED, LOG_OUT } from '../actions/auth'
+import { DELETE_CURRENT_USER_SUCCEEDED } from '../actions/users'
+
+const getAuthObject = () => {
+ const authItem = localStorage.getItem('auth')
+ if (!authItem || authItem === '{}') {
+ return undefined
+ }
+ return JSON.parse(authItem)
+}
+
+export const userIsLoggedIn = () => {
+ const authObj = getAuthObject()
+
+ if (!authObj || !authObj.googleId) {
+ return false
+ }
+
+ const currentTime = new Date().getTime()
+ return parseInt(authObj.expiresAt, 10) - currentTime > 0
+}
+
+export const getAuthToken = () => {
+ const authObj = getAuthObject()
+ if (!authObj) {
+ return undefined
+ }
+
+ return authObj.authToken
+}
+
+export const saveAuthLocalStorage = (payload) => {
+ localStorage.setItem('auth', JSON.stringify(payload))
+}
+
+export const clearAuthLocalStorage = () => {
+ localStorage.setItem('auth', '')
+}
+
+export const authRedirectMiddleware = (store) => (next) => (action) => {
+ switch (action.type) {
+ case LOG_IN_SUCCEEDED:
+ saveAuthLocalStorage(action.payload)
+ window.location.href = '/projects'
+ break
+ case LOG_OUT:
+ case DELETE_CURRENT_USER_SUCCEEDED:
+ clearAuthLocalStorage()
+ window.location.href = '/'
+ break
+ default:
+ next(action)
+ return
+ }
+
+ next(action)
+}
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js b/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js
new file mode 100644
index 00000000..7efea9b0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import FontAwesome from 'react-fontawesome'
+
+const LoadingScreen = () => (
+ <div className="display-4">
+ <FontAwesome name="refresh" className="mr-4" spin />
+ Loading your project...
+ </div>
+)
+
+export default LoadingScreen
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js
new file mode 100644
index 00000000..d6ea1f84
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js
@@ -0,0 +1,28 @@
+export const MAP_SIZE = 50
+export const TILE_SIZE_IN_PIXELS = 100
+export const TILE_SIZE_IN_METERS = 0.5
+export const MAP_SIZE_IN_PIXELS = MAP_SIZE * TILE_SIZE_IN_PIXELS
+
+export const OBJECT_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 5
+export const TILE_PLUS_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 3
+export const OBJECT_SIZE_IN_PIXELS = TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2
+
+export const GRID_LINE_WIDTH_IN_PIXELS = 2
+export const WALL_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 8
+export const OBJECT_BORDER_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 12
+export const TILE_PLUS_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 10
+
+export const SIDEBAR_WIDTH = 350
+export const VIEWPORT_PADDING = 50
+
+export const RACK_FILL_ICON_WIDTH = OBJECT_SIZE_IN_PIXELS / 3
+export const RACK_FILL_ICON_OPACITY = 0.8
+
+export const MAP_MOVE_PIXELS_PER_EVENT = 20
+export const MAP_SCALE_PER_EVENT = 1.1
+export const MAP_MIN_SCALE = 0.5
+export const MAP_MAX_SCALE = 1.5
+
+export const MAX_NUM_UNITS_PER_MACHINE = 6
+export const DEFAULT_RACK_SLOT_CAPACITY = 42
+export const DEFAULT_RACK_POWER_CAPACITY = 10000
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js
new file mode 100644
index 00000000..2cd0ed6e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js
@@ -0,0 +1,103 @@
+import React from 'react'
+import { Stage } from 'react-konva'
+import { Shortcuts } from 'react-shortcuts'
+import MapLayer from '../../../containers/app/map/layers/MapLayer'
+import ObjectHoverLayer from '../../../containers/app/map/layers/ObjectHoverLayer'
+import RoomHoverLayer from '../../../containers/app/map/layers/RoomHoverLayer'
+import { NAVBAR_HEIGHT } from '../../navigation/Navbar'
+import { MAP_MOVE_PIXELS_PER_EVENT } from './MapConstants'
+import { Provider } from 'react-redux'
+import { store } from '../../../store/configure-store'
+
+class MapStageComponent extends React.Component {
+ state = {
+ mouseX: 0,
+ mouseY: 0,
+ }
+
+ constructor(props) {
+ super(props)
+
+ this.updateDimensions = this.updateDimensions.bind(this)
+ this.updateScale = this.updateScale.bind(this)
+ }
+
+ componentDidMount() {
+ this.updateDimensions()
+
+ window.addEventListener('resize', this.updateDimensions)
+ window.addEventListener('wheel', this.updateScale)
+
+ window['exportCanvasToImage'] = () => {
+ const download = document.createElement('a')
+ download.href = this.stage.getStage().toDataURL()
+ download.download = 'opendc-canvas-export-' + Date.now() + '.png'
+ download.click()
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.updateDimensions)
+ window.removeEventListener('wheel', this.updateScale)
+ }
+
+ updateDimensions() {
+ this.props.setMapDimensions(window.innerWidth, window.innerHeight - NAVBAR_HEIGHT)
+ }
+
+ updateScale(e) {
+ e.preventDefault()
+ this.props.zoomInOnPosition(e.deltaY < 0, this.state.mouseX, this.state.mouseY)
+ }
+
+ updateMousePosition() {
+ const mousePos = this.stage.getStage().getPointerPosition()
+ this.setState({ mouseX: mousePos.x, mouseY: mousePos.y })
+ }
+
+ handleShortcuts(action) {
+ switch (action) {
+ case 'MOVE_LEFT':
+ this.moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0)
+ break
+ case 'MOVE_RIGHT':
+ this.moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0)
+ break
+ case 'MOVE_UP':
+ this.moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT)
+ break
+ case 'MOVE_DOWN':
+ this.moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT)
+ break
+ default:
+ break
+ }
+ }
+
+ moveWithDelta(deltaX, deltaY) {
+ this.props.setMapPositionWithBoundsCheck(this.props.mapPosition.x + deltaX, this.props.mapPosition.y + deltaY)
+ }
+
+ render() {
+ return (
+ <Shortcuts name="MAP" handler={this.handleShortcuts.bind(this)} targetNodeSelector="body">
+ <Stage
+ ref={(stage) => {
+ this.stage = stage
+ }}
+ width={this.props.mapDimensions.width}
+ height={this.props.mapDimensions.height}
+ onMouseMove={this.updateMousePosition.bind(this)}
+ >
+ <Provider store={store}>
+ <MapLayer />
+ <RoomHoverLayer mouseX={this.state.mouseX} mouseY={this.state.mouseY} />
+ <ObjectHoverLayer mouseX={this.state.mouseX} mouseY={this.state.mouseY} />
+ </Provider>
+ </Stage>
+ </Shortcuts>
+ )
+ }
+}
+
+export default MapStageComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js
new file mode 100644
index 00000000..8487f47b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js
@@ -0,0 +1,13 @@
+import React from 'react'
+
+const ExportCanvasComponent = () => (
+ <button
+ className="btn btn-success btn-circle btn-sm"
+ title="Export Canvas to PNG Image"
+ onClick={() => window['exportCanvasToImage']()}
+ >
+ <span className="fa fa-camera" />
+ </button>
+)
+
+export default ExportCanvasComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js
new file mode 100644
index 00000000..7cbb45c0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import { TILE_SIZE_IN_METERS, TILE_SIZE_IN_PIXELS } from '../MapConstants'
+import './ScaleIndicatorComponent.sass'
+
+const ScaleIndicatorComponent = ({ scale }) => (
+ <div className="scale-indicator" style={{ width: TILE_SIZE_IN_PIXELS * scale }}>
+ {TILE_SIZE_IN_METERS}m
+ </div>
+)
+
+export default ScaleIndicatorComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.sass b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.sass
new file mode 100644
index 00000000..03a72c99
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.sass
@@ -0,0 +1,9 @@
+.scale-indicator
+ position: absolute
+ right: 10px
+ bottom: 10px
+ z-index: 50
+
+ border: solid 2px #212529
+ border-top: none
+ border-left: none
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js
new file mode 100644
index 00000000..f372734d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import ZoomControlContainer from '../../../../containers/app/map/controls/ZoomControlContainer'
+import ExportCanvasComponent from './ExportCanvasComponent'
+import './ToolPanelComponent.sass'
+
+const ToolPanelComponent = () => (
+ <div className="tool-panel">
+ <ZoomControlContainer />
+ <ExportCanvasComponent />
+ </div>
+)
+
+export default ToolPanelComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.sass b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.sass
new file mode 100644
index 00000000..8b27d24a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.sass
@@ -0,0 +1,5 @@
+.tool-panel
+ position: absolute
+ left: 10px
+ bottom: 10px
+ z-index: 50
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js
new file mode 100644
index 00000000..65944bea
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js
@@ -0,0 +1,24 @@
+import React from 'react'
+
+const ZoomControlComponent = ({ zoomInOnCenter }) => {
+ return (
+ <span>
+ <button
+ className="btn btn-default btn-circle btn-sm mr-1"
+ title="Zoom in"
+ onClick={() => zoomInOnCenter(true)}
+ >
+ <span className="fa fa-plus" />
+ </button>
+ <button
+ className="btn btn-default btn-circle btn-sm mr-1"
+ title="Zoom out"
+ onClick={() => zoomInOnCenter(false)}
+ >
+ <span className="fa fa-minus" />
+ </button>
+ </span>
+ )
+}
+
+export default ZoomControlComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/Backdrop.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/Backdrop.js
new file mode 100644
index 00000000..8ccfe584
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/Backdrop.js
@@ -0,0 +1,8 @@
+import React from 'react'
+import { Rect } from 'react-konva'
+import { BACKDROP_COLOR } from '../../../../util/colors'
+import { MAP_SIZE_IN_PIXELS } from '../MapConstants'
+
+const Backdrop = () => <Rect x={0} y={0} width={MAP_SIZE_IN_PIXELS} height={MAP_SIZE_IN_PIXELS} fill={BACKDROP_COLOR} />
+
+export default Backdrop
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/GrayLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/GrayLayer.js
new file mode 100644
index 00000000..c54a34ad
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/GrayLayer.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import { Rect } from 'react-konva'
+import { GRAYED_OUT_AREA_COLOR } from '../../../../util/colors'
+import { MAP_SIZE_IN_PIXELS } from '../MapConstants'
+
+const GrayLayer = ({ onClick }) => (
+ <Rect
+ x={0}
+ y={0}
+ width={MAP_SIZE_IN_PIXELS}
+ height={MAP_SIZE_IN_PIXELS}
+ fill={GRAYED_OUT_AREA_COLOR}
+ onClick={onClick}
+ />
+)
+
+export default GrayLayer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js
new file mode 100644
index 00000000..912229c4
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js
@@ -0,0 +1,27 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Rect } from 'react-konva'
+import { ROOM_HOVER_INVALID_COLOR, ROOM_HOVER_VALID_COLOR } from '../../../../util/colors'
+import { TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+const HoverTile = ({ pixelX, pixelY, isValid, scale, onClick }) => (
+ <Rect
+ x={pixelX}
+ y={pixelY}
+ scaleX={scale}
+ scaleY={scale}
+ width={TILE_SIZE_IN_PIXELS}
+ height={TILE_SIZE_IN_PIXELS}
+ fill={isValid ? ROOM_HOVER_VALID_COLOR : ROOM_HOVER_INVALID_COLOR}
+ onClick={onClick}
+ />
+)
+
+HoverTile.propTypes = {
+ pixelX: PropTypes.number.isRequired,
+ pixelY: PropTypes.number.isRequired,
+ isValid: PropTypes.bool.isRequired,
+ onClick: PropTypes.func.isRequired,
+}
+
+export default HoverTile
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/ImageComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/ImageComponent.js
new file mode 100644
index 00000000..2b5c569f
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/ImageComponent.js
@@ -0,0 +1,48 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Image } from 'react-konva'
+
+class ImageComponent extends React.Component {
+ static imageCaches = {}
+ static propTypes = {
+ src: PropTypes.string.isRequired,
+ x: PropTypes.number.isRequired,
+ y: PropTypes.number.isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired,
+ opacity: PropTypes.number.isRequired,
+ }
+
+ state = {
+ image: null,
+ }
+
+ componentDidMount() {
+ if (ImageComponent.imageCaches[this.props.src]) {
+ this.setState({ image: ImageComponent.imageCaches[this.props.src] })
+ return
+ }
+
+ const image = new window.Image()
+ image.src = this.props.src
+ image.onload = () => {
+ this.setState({ image })
+ ImageComponent.imageCaches[this.props.src] = image
+ }
+ }
+
+ render() {
+ return (
+ <Image
+ image={this.state.image}
+ x={this.props.x}
+ y={this.props.y}
+ width={this.props.width}
+ height={this.props.height}
+ opacity={this.props.opacity}
+ />
+ )
+ }
+}
+
+export default ImageComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js
new file mode 100644
index 00000000..8c573a6f
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js
@@ -0,0 +1,68 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Group, Rect } from 'react-konva'
+import {
+ RACK_ENERGY_BAR_BACKGROUND_COLOR,
+ RACK_ENERGY_BAR_FILL_COLOR,
+ RACK_SPACE_BAR_BACKGROUND_COLOR,
+ RACK_SPACE_BAR_FILL_COLOR,
+} from '../../../../util/colors'
+import {
+ OBJECT_BORDER_WIDTH_IN_PIXELS,
+ OBJECT_MARGIN_IN_PIXELS,
+ RACK_FILL_ICON_OPACITY,
+ RACK_FILL_ICON_WIDTH,
+ TILE_SIZE_IN_PIXELS,
+} from '../MapConstants'
+import ImageComponent from './ImageComponent'
+
+const RackFillBar = ({ positionX, positionY, type, fillFraction }) => {
+ const halfOfObjectBorderWidth = OBJECT_BORDER_WIDTH_IN_PIXELS / 2
+ const x =
+ positionX * TILE_SIZE_IN_PIXELS +
+ OBJECT_MARGIN_IN_PIXELS +
+ (type === 'space' ? halfOfObjectBorderWidth : 0.5 * (TILE_SIZE_IN_PIXELS - 2 * OBJECT_MARGIN_IN_PIXELS))
+ const startY = positionY * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS + halfOfObjectBorderWidth
+ const width = 0.5 * (TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2) - halfOfObjectBorderWidth
+ const fullHeight = TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2 - OBJECT_BORDER_WIDTH_IN_PIXELS
+
+ const fractionHeight = fillFraction * fullHeight
+ const fractionY =
+ (positionY + 1) * TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS - halfOfObjectBorderWidth - fractionHeight
+
+ return (
+ <Group>
+ <Rect
+ x={x}
+ y={startY}
+ width={width}
+ height={fullHeight}
+ fill={type === 'space' ? RACK_SPACE_BAR_BACKGROUND_COLOR : RACK_ENERGY_BAR_BACKGROUND_COLOR}
+ />
+ <Rect
+ x={x}
+ y={fractionY}
+ width={width}
+ height={fractionHeight}
+ fill={type === 'space' ? RACK_SPACE_BAR_FILL_COLOR : RACK_ENERGY_BAR_FILL_COLOR}
+ />
+ <ImageComponent
+ src={'/img/topology/rack-' + type + '-icon.png'}
+ x={x + width * 0.5 - RACK_FILL_ICON_WIDTH * 0.5}
+ y={startY + fullHeight * 0.5 - RACK_FILL_ICON_WIDTH * 0.5}
+ width={RACK_FILL_ICON_WIDTH}
+ height={RACK_FILL_ICON_WIDTH}
+ opacity={RACK_FILL_ICON_OPACITY}
+ />
+ </Group>
+ )
+}
+
+RackFillBar.propTypes = {
+ positionX: PropTypes.number.isRequired,
+ positionY: PropTypes.number.isRequired,
+ type: PropTypes.string.isRequired,
+ fillFraction: PropTypes.number.isRequired,
+}
+
+export default RackFillBar
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/RoomTile.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/RoomTile.js
new file mode 100644
index 00000000..43bf918e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/RoomTile.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import { Rect } from 'react-konva'
+import Shapes from '../../../../shapes/index'
+import { TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+const RoomTile = ({ tile, color }) => (
+ <Rect
+ x={tile.positionX * TILE_SIZE_IN_PIXELS}
+ y={tile.positionY * TILE_SIZE_IN_PIXELS}
+ width={TILE_SIZE_IN_PIXELS}
+ height={TILE_SIZE_IN_PIXELS}
+ fill={color}
+ />
+)
+
+RoomTile.propTypes = {
+ tile: Shapes.Tile,
+}
+
+export default RoomTile
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/TileObject.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/TileObject.js
new file mode 100644
index 00000000..9e87cc82
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/TileObject.js
@@ -0,0 +1,25 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Rect } from 'react-konva'
+import { OBJECT_BORDER_COLOR } from '../../../../util/colors'
+import { OBJECT_BORDER_WIDTH_IN_PIXELS, OBJECT_MARGIN_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+const TileObject = ({ positionX, positionY, color }) => (
+ <Rect
+ x={positionX * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS}
+ y={positionY * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS}
+ width={TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2}
+ height={TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2}
+ fill={color}
+ stroke={OBJECT_BORDER_COLOR}
+ strokeWidth={OBJECT_BORDER_WIDTH_IN_PIXELS}
+ />
+)
+
+TileObject.propTypes = {
+ positionX: PropTypes.number.isRequired,
+ positionY: PropTypes.number.isRequired,
+ color: PropTypes.string.isRequired,
+}
+
+export default TileObject
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js
new file mode 100644
index 00000000..be3a00a8
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js
@@ -0,0 +1,44 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Group, Line } from 'react-konva'
+import { TILE_PLUS_COLOR } from '../../../../util/colors'
+import { TILE_PLUS_MARGIN_IN_PIXELS, TILE_PLUS_WIDTH_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+const TilePlusIcon = ({ pixelX, pixelY, mapScale }) => {
+ const linePoints = [
+ [
+ pixelX + 0.5 * TILE_SIZE_IN_PIXELS * mapScale,
+ pixelY + TILE_PLUS_MARGIN_IN_PIXELS * mapScale,
+ pixelX + 0.5 * TILE_SIZE_IN_PIXELS * mapScale,
+ pixelY + TILE_SIZE_IN_PIXELS * mapScale - TILE_PLUS_MARGIN_IN_PIXELS * mapScale,
+ ],
+ [
+ pixelX + TILE_PLUS_MARGIN_IN_PIXELS * mapScale,
+ pixelY + 0.5 * TILE_SIZE_IN_PIXELS * mapScale,
+ pixelX + TILE_SIZE_IN_PIXELS * mapScale - TILE_PLUS_MARGIN_IN_PIXELS * mapScale,
+ pixelY + 0.5 * TILE_SIZE_IN_PIXELS * mapScale,
+ ],
+ ]
+ return (
+ <Group>
+ {linePoints.map((points, index) => (
+ <Line
+ key={index}
+ points={points}
+ lineCap="round"
+ stroke={TILE_PLUS_COLOR}
+ strokeWidth={TILE_PLUS_WIDTH_IN_PIXELS * mapScale}
+ listening={false}
+ />
+ ))}
+ </Group>
+ )
+}
+
+TilePlusIcon.propTypes = {
+ pixelX: PropTypes.number,
+ pixelY: PropTypes.number,
+ mapScale: PropTypes.number,
+}
+
+export default TilePlusIcon
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/WallSegment.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/WallSegment.js
new file mode 100644
index 00000000..8aa2aebf
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/WallSegment.js
@@ -0,0 +1,32 @@
+import React from 'react'
+import { Line } from 'react-konva'
+import Shapes from '../../../../shapes/index'
+import { WALL_COLOR } from '../../../../util/colors'
+import { TILE_SIZE_IN_PIXELS, WALL_WIDTH_IN_PIXELS } from '../MapConstants'
+
+const WallSegment = ({ wallSegment }) => {
+ let points
+ if (wallSegment.isHorizontal) {
+ points = [
+ wallSegment.startPosX * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosY * TILE_SIZE_IN_PIXELS,
+ (wallSegment.startPosX + wallSegment.length) * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosY * TILE_SIZE_IN_PIXELS,
+ ]
+ } else {
+ points = [
+ wallSegment.startPosX * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosY * TILE_SIZE_IN_PIXELS,
+ wallSegment.startPosX * TILE_SIZE_IN_PIXELS,
+ (wallSegment.startPosY + wallSegment.length) * TILE_SIZE_IN_PIXELS,
+ ]
+ }
+
+ return <Line points={points} lineCap="round" stroke={WALL_COLOR} strokeWidth={WALL_WIDTH_IN_PIXELS} />
+}
+
+WallSegment.propTypes = {
+ wallSegment: Shapes.WallSegment,
+}
+
+export default WallSegment
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/GridGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/GridGroup.js
new file mode 100644
index 00000000..ebc00244
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/GridGroup.js
@@ -0,0 +1,34 @@
+import React from 'react'
+import { Group, Line } from 'react-konva'
+import { GRID_COLOR } from '../../../../util/colors'
+import { GRID_LINE_WIDTH_IN_PIXELS, MAP_SIZE, MAP_SIZE_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+const MAP_COORDINATE_ENTRIES = Array.from(new Array(MAP_SIZE), (x, i) => i)
+const HORIZONTAL_POINT_PAIRS = MAP_COORDINATE_ENTRIES.map((index) => [
+ 0,
+ index * TILE_SIZE_IN_PIXELS,
+ MAP_SIZE_IN_PIXELS,
+ index * TILE_SIZE_IN_PIXELS,
+])
+const VERTICAL_POINT_PAIRS = MAP_COORDINATE_ENTRIES.map((index) => [
+ index * TILE_SIZE_IN_PIXELS,
+ 0,
+ index * TILE_SIZE_IN_PIXELS,
+ MAP_SIZE_IN_PIXELS,
+])
+
+const GridGroup = () => (
+ <Group>
+ {HORIZONTAL_POINT_PAIRS.concat(VERTICAL_POINT_PAIRS).map((points, index) => (
+ <Line
+ key={index}
+ points={points}
+ stroke={GRID_COLOR}
+ strokeWidth={GRID_LINE_WIDTH_IN_PIXELS}
+ listening={false}
+ />
+ ))}
+ </Group>
+)
+
+export default GridGroup
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js
new file mode 100644
index 00000000..eb6dc24a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import { Group } from 'react-konva'
+import RackEnergyFillContainer from '../../../../containers/app/map/RackEnergyFillContainer'
+import RackSpaceFillContainer from '../../../../containers/app/map/RackSpaceFillContainer'
+import Shapes from '../../../../shapes/index'
+import { RACK_BACKGROUND_COLOR } from '../../../../util/colors'
+import TileObject from '../elements/TileObject'
+
+const RackGroup = ({ tile }) => {
+ return (
+ <Group>
+ <TileObject positionX={tile.positionX} positionY={tile.positionY} color={RACK_BACKGROUND_COLOR} />
+ <Group>
+ <RackSpaceFillContainer tileId={tile._id} positionX={tile.positionX} positionY={tile.positionY} />
+ <RackEnergyFillContainer tileId={tile._id} positionX={tile.positionX} positionY={tile.positionY} />
+ </Group>
+ </Group>
+ )
+}
+
+RackGroup.propTypes = {
+ tile: Shapes.Tile,
+}
+
+export default RackGroup
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js
new file mode 100644
index 00000000..1fd54687
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js
@@ -0,0 +1,48 @@
+import React from 'react'
+import { Group } from 'react-konva'
+import GrayContainer from '../../../../containers/app/map/GrayContainer'
+import TileContainer from '../../../../containers/app/map/TileContainer'
+import WallContainer from '../../../../containers/app/map/WallContainer'
+import Shapes from '../../../../shapes/index'
+
+const RoomGroup = ({ room, interactionLevel, currentRoomInConstruction, onClick }) => {
+ if (currentRoomInConstruction === room._id) {
+ return (
+ <Group onClick={onClick}>
+ {room.tileIds.map((tileId) => (
+ <TileContainer key={tileId} tileId={tileId} newTile={true} />
+ ))}
+ </Group>
+ )
+ }
+
+ return (
+ <Group onClick={onClick}>
+ {(() => {
+ if (
+ (interactionLevel.mode === 'RACK' || interactionLevel.mode === 'MACHINE') &&
+ interactionLevel.roomId === room._id
+ ) {
+ return [
+ room.tileIds
+ .filter((tileId) => tileId !== interactionLevel.tileId)
+ .map((tileId) => <TileContainer key={tileId} tileId={tileId} />),
+ <GrayContainer key={-1} />,
+ room.tileIds
+ .filter((tileId) => tileId === interactionLevel.tileId)
+ .map((tileId) => <TileContainer key={tileId} tileId={tileId} />),
+ ]
+ } else {
+ return room.tileIds.map((tileId) => <TileContainer key={tileId} tileId={tileId} />)
+ }
+ })()}
+ <WallContainer roomId={room._id} />
+ </Group>
+ )
+}
+
+RoomGroup.propTypes = {
+ room: Shapes.Room,
+}
+
+export default RoomGroup
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js
new file mode 100644
index 00000000..1e106823
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js
@@ -0,0 +1,35 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Group } from 'react-konva'
+import RackContainer from '../../../../containers/app/map/RackContainer'
+import Shapes from '../../../../shapes/index'
+import { ROOM_DEFAULT_COLOR, ROOM_IN_CONSTRUCTION_COLOR } from '../../../../util/colors'
+import RoomTile from '../elements/RoomTile'
+
+const TileGroup = ({ tile, newTile, roomLoad, onClick }) => {
+ let tileObject
+ if (tile.rackId) {
+ tileObject = <RackContainer tile={tile} />
+ } else {
+ tileObject = null
+ }
+
+ let color = ROOM_DEFAULT_COLOR
+ if (newTile) {
+ color = ROOM_IN_CONSTRUCTION_COLOR
+ }
+
+ return (
+ <Group onClick={() => onClick(tile)}>
+ <RoomTile tile={tile} color={color} />
+ {tileObject}
+ </Group>
+ )
+}
+
+TileGroup.propTypes = {
+ tile: Shapes.Tile,
+ newTile: PropTypes.bool,
+}
+
+export default TileGroup
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js
new file mode 100644
index 00000000..6096fc8b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import { Group } from 'react-konva'
+import GrayContainer from '../../../../containers/app/map/GrayContainer'
+import RoomContainer from '../../../../containers/app/map/RoomContainer'
+import Shapes from '../../../../shapes/index'
+
+const TopologyGroup = ({ topology, interactionLevel }) => {
+ if (!topology) {
+ return <Group />
+ }
+
+ if (interactionLevel.mode === 'BUILDING') {
+ return (
+ <Group>
+ {topology.roomIds.map((roomId) => (
+ <RoomContainer key={roomId} roomId={roomId} />
+ ))}
+ </Group>
+ )
+ }
+
+ return (
+ <Group>
+ {topology.roomIds
+ .filter((roomId) => roomId !== interactionLevel.roomId)
+ .map((roomId) => (
+ <RoomContainer key={roomId} roomId={roomId} />
+ ))}
+ {interactionLevel.mode === 'ROOM' ? <GrayContainer /> : null}
+ {topology.roomIds
+ .filter((roomId) => roomId === interactionLevel.roomId)
+ .map((roomId) => (
+ <RoomContainer key={roomId} roomId={roomId} />
+ ))}
+ </Group>
+ )
+}
+
+TopologyGroup.propTypes = {
+ topology: Shapes.Topology,
+ interactionLevel: Shapes.InteractionLevel,
+}
+
+export default TopologyGroup
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/WallGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/WallGroup.js
new file mode 100644
index 00000000..7b0f5ca0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/WallGroup.js
@@ -0,0 +1,22 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Group } from 'react-konva'
+import Shapes from '../../../../shapes/index'
+import { deriveWallLocations } from '../../../../util/tile-calculations'
+import WallSegment from '../elements/WallSegment'
+
+const WallGroup = ({ tiles }) => {
+ return (
+ <Group>
+ {deriveWallLocations(tiles).map((wallSegment, index) => (
+ <WallSegment key={index} wallSegment={wallSegment} />
+ ))}
+ </Group>
+ )
+}
+
+WallGroup.propTypes = {
+ tiles: PropTypes.arrayOf(Shapes.Tile).isRequired,
+}
+
+export default WallGroup
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js
new file mode 100644
index 00000000..bead87de
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js
@@ -0,0 +1,75 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Layer } from 'react-konva'
+import HoverTile from '../elements/HoverTile'
+import { TILE_SIZE_IN_PIXELS } from '../MapConstants'
+
+class HoverLayerComponent extends React.Component {
+ static propTypes = {
+ mouseX: PropTypes.number.isRequired,
+ mouseY: PropTypes.number.isRequired,
+ mapPosition: PropTypes.object.isRequired,
+ mapScale: PropTypes.number.isRequired,
+ isEnabled: PropTypes.func.isRequired,
+ onClick: PropTypes.func.isRequired,
+ }
+
+ state = {
+ positionX: -1,
+ positionY: -1,
+ validity: false,
+ }
+
+ componentDidUpdate() {
+ if (!this.props.isEnabled()) {
+ return
+ }
+
+ const positionX = Math.floor(
+ (this.props.mouseX - this.props.mapPosition.x) / (this.props.mapScale * TILE_SIZE_IN_PIXELS)
+ )
+ const positionY = Math.floor(
+ (this.props.mouseY - this.props.mapPosition.y) / (this.props.mapScale * TILE_SIZE_IN_PIXELS)
+ )
+
+ if (positionX !== this.state.positionX || positionY !== this.state.positionY) {
+ this.setState({
+ positionX,
+ positionY,
+ validity: this.props.isValid(positionX, positionY),
+ })
+ }
+ }
+
+ render() {
+ if (!this.props.isEnabled()) {
+ return <Layer />
+ }
+
+ const pixelX = this.props.mapScale * this.state.positionX * TILE_SIZE_IN_PIXELS + this.props.mapPosition.x
+ const pixelY = this.props.mapScale * this.state.positionY * TILE_SIZE_IN_PIXELS + this.props.mapPosition.y
+
+ return (
+ <Layer opacity={0.6}>
+ <HoverTile
+ pixelX={pixelX}
+ pixelY={pixelY}
+ scale={this.props.mapScale}
+ isValid={this.state.validity}
+ onClick={() =>
+ this.state.validity ? this.props.onClick(this.state.positionX, this.state.positionY) : undefined
+ }
+ />
+ {this.props.children
+ ? React.cloneElement(this.props.children, {
+ pixelX,
+ pixelY,
+ scale: this.props.mapScale,
+ })
+ : undefined}
+ </Layer>
+ )
+ }
+}
+
+export default HoverLayerComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js
new file mode 100644
index 00000000..8ee14c9c
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import { Group, Layer } from 'react-konva'
+import TopologyContainer from '../../../../containers/app/map/TopologyContainer'
+import Backdrop from '../elements/Backdrop'
+import GridGroup from '../groups/GridGroup'
+
+const MapLayerComponent = ({ mapPosition, mapScale }) => (
+ <Layer>
+ <Group x={mapPosition.x} y={mapPosition.y} scaleX={mapScale} scaleY={mapScale}>
+ <Backdrop />
+ <TopologyContainer />
+ <GridGroup />
+ </Group>
+ </Layer>
+)
+
+export default MapLayerComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js
new file mode 100644
index 00000000..661fc255
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import TilePlusIcon from '../elements/TilePlusIcon'
+import HoverLayerComponent from './HoverLayerComponent'
+
+const ObjectHoverLayerComponent = (props) => (
+ <HoverLayerComponent {...props}>
+ <TilePlusIcon {...props} />
+ </HoverLayerComponent>
+)
+
+export default ObjectHoverLayerComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js
new file mode 100644
index 00000000..887e2891
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js
@@ -0,0 +1,6 @@
+import React from 'react'
+import HoverLayerComponent from './HoverLayerComponent'
+
+const RoomHoverLayerComponent = (props) => <HoverLayerComponent {...props} />
+
+export default RoomHoverLayerComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js
new file mode 100644
index 00000000..759acd57
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js
@@ -0,0 +1,93 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Bar, CartesianGrid, ComposedChart, ErrorBar, ResponsiveContainer, Scatter, XAxis, YAxis } from 'recharts'
+import { AVAILABLE_METRICS, METRIC_NAMES_SHORT, METRIC_UNITS } from '../../../util/available-metrics'
+import { mean, std } from 'mathjs'
+import Shapes from '../../../shapes/index'
+import approx from 'approximate-number'
+
+const PortfolioResultsComponent = ({ portfolio, scenarios }) => {
+ if (!portfolio) {
+ return <div>Loading...</div>
+ }
+
+ const nonFinishedScenarios = scenarios.filter((s) => s.simulation.state !== 'FINISHED')
+
+ if (nonFinishedScenarios.length > 0) {
+ if (nonFinishedScenarios.every((s) => s.simulation.state === 'QUEUED' || s.simulation.state === 'RUNNING')) {
+ return (
+ <div>
+ <h1>Simulation running...</h1>
+ <p>{nonFinishedScenarios.length} of the scenarios are still being simulated</p>
+ </div>
+ )
+ }
+ if (nonFinishedScenarios.some((s) => s.simulation.state === 'FAILED')) {
+ return (
+ <div>
+ <h1>Simulation failed.</h1>
+ <p>
+ Try again by creating a new scenario. Please contact the OpenDC team for support, if issues
+ persist.
+ </p>
+ </div>
+ )
+ }
+ }
+
+ const dataPerMetric = {}
+
+ AVAILABLE_METRICS.forEach((metric) => {
+ dataPerMetric[metric] = scenarios.map((scenario) => ({
+ name: scenario.name,
+ value: mean(scenario.results[metric]),
+ errorX: std(scenario.results[metric]),
+ }))
+ })
+
+ return (
+ <div className="full-height" style={{ overflowY: 'scroll', overflowX: 'hidden' }}>
+ <h2>Portfolio: {portfolio.name}</h2>
+ <p>Repeats per Scenario: {portfolio.targets.repeatsPerScenario}</p>
+ <div className="row">
+ {AVAILABLE_METRICS.map((metric) => (
+ <div className="col-6 mb-2" key={metric}>
+ <h4>{METRIC_NAMES_SHORT[metric]}</h4>
+ <ResponsiveContainer aspect={16 / 9} width="100%">
+ <ComposedChart
+ data={dataPerMetric[metric]}
+ margin={{ left: 35, bottom: 15 }}
+ layout="vertical"
+ >
+ <CartesianGrid strokeDasharray="3 3" />
+ <XAxis
+ tickFormatter={(tick) => approx(tick)}
+ label={{ value: METRIC_UNITS[metric], position: 'bottom', offset: 0 }}
+ type="number"
+ />
+ <YAxis dataKey="name" type="category" />
+ <Bar dataKey="value" fill="#3399FF" isAnimationActive={false} />
+ <Scatter dataKey="value" opacity={0} isAnimationActive={false}>
+ <ErrorBar
+ dataKey="errorX"
+ width={10}
+ strokeWidth={3}
+ stroke="#FF6600"
+ direction="x"
+ />
+ </Scatter>
+ </ComposedChart>
+ </ResponsiveContainer>
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+}
+
+PortfolioResultsComponent.propTypes = {
+ portfolio: Shapes.Portfolio,
+ scenarios: PropTypes.arrayOf(Shapes.Scenario),
+}
+
+export default PortfolioResultsComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js
new file mode 100644
index 00000000..f7368f54
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js
@@ -0,0 +1,53 @@
+import PropTypes from 'prop-types'
+import classNames from 'classnames'
+import React from 'react'
+import './Sidebar.sass'
+
+class Sidebar extends React.Component {
+ static propTypes = {
+ isRight: PropTypes.bool.isRequired,
+ collapsible: PropTypes.bool,
+ }
+
+ static defaultProps = {
+ collapsible: true,
+ }
+
+ state = {
+ collapsed: false,
+ }
+
+ render() {
+ const collapseButton = (
+ <div
+ className={classNames('sidebar-collapse-button', {
+ 'sidebar-collapse-button-right': this.props.isRight,
+ })}
+ onClick={() => this.setState({ collapsed: !this.state.collapsed })}
+ >
+ {(this.state.collapsed && this.props.isRight) || (!this.state.collapsed && !this.props.isRight) ? (
+ <span className="fa fa-angle-left" title={this.props.isRight ? 'Expand' : 'Collapse'} />
+ ) : (
+ <span className="fa fa-angle-right" title={this.props.isRight ? 'Collapse' : 'Expand'} />
+ )}
+ </div>
+ )
+
+ if (this.state.collapsed) {
+ return collapseButton
+ }
+ return (
+ <div
+ className={classNames('sidebar p-3 h-100', {
+ 'sidebar-right': this.props.isRight,
+ })}
+ onWheel={(e) => e.stopPropagation()}
+ >
+ {this.props.children}
+ {this.props.collapsible && collapseButton}
+ </div>
+ )
+ }
+}
+
+export default Sidebar
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.sass b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.sass
new file mode 100644
index 00000000..b8e15716
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.sass
@@ -0,0 +1,50 @@
+@import ../../../style-globals/_variables.sass
+@import ../../../style-globals/_mixins.sass
+
+.sidebar-collapse-button
+ position: absolute
+ left: 5px
+ top: 5px
+ padding: 5px 7px
+
+ background: white
+ border: solid 1px $gray-semi-light
+ z-index: 99
+
+ +clickable
+ +border-radius(5px)
+ +transition(background, 200ms)
+
+ &.sidebar-collapse-button-right
+ left: auto
+ right: 5px
+ top: 5px
+
+ &:hover
+ background: #eeeeee
+
+.sidebar
+ position: absolute
+ top: 0
+ left: 0
+ width: $side-bar-width
+
+ z-index: 100
+ background: white
+
+ border-right: $gray-semi-dark 1px solid
+
+ .sidebar-collapse-button
+ left: auto
+ right: -25px
+
+.sidebar-right
+ left: auto
+ right: 0
+
+ border-left: $gray-semi-dark 1px solid
+ border-right: none
+
+ .sidebar-collapse-button-right
+ left: -25px
+ right: auto
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js
new file mode 100644
index 00000000..b000b9e2
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js
@@ -0,0 +1,66 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import Shapes from '../../../../shapes'
+import { Link } from 'react-router-dom'
+import FontAwesome from 'react-fontawesome'
+import ScenarioListContainer from '../../../../containers/app/sidebars/project/ScenarioListContainer'
+
+class PortfolioListComponent extends React.Component {
+ static propTypes = {
+ portfolios: PropTypes.arrayOf(Shapes.Portfolio),
+ currentProjectId: PropTypes.string.isRequired,
+ currentPortfolioId: PropTypes.string,
+ onNewPortfolio: PropTypes.func.isRequired,
+ onChoosePortfolio: PropTypes.func.isRequired,
+ onDeletePortfolio: PropTypes.func.isRequired,
+ }
+
+ onDelete(id) {
+ this.props.onDeletePortfolio(id)
+ }
+
+ render() {
+ return (
+ <div className="pb-3">
+ <h2>
+ Portfolios
+ <button
+ className="btn btn-outline-primary float-right"
+ onClick={this.props.onNewPortfolio.bind(this)}
+ >
+ <FontAwesome name="plus" />
+ </button>
+ </h2>
+
+ {this.props.portfolios.map((portfolio, idx) => (
+ <div key={portfolio._id}>
+ <div className="row mb-1">
+ <div
+ className={
+ 'col-7 align-self-center ' +
+ (portfolio._id === this.props.currentPortfolioId ? 'font-weight-bold' : '')
+ }
+ >
+ {portfolio.name}
+ </div>
+ <div className="col-5 text-right">
+ <Link
+ className="btn btn-outline-primary mr-1 fa fa-play"
+ to={`/projects/${this.props.currentProjectId}/portfolios/${portfolio._id}`}
+ onClick={() => this.props.onChoosePortfolio(portfolio._id)}
+ />
+ <span
+ className="btn btn-outline-danger fa fa-trash"
+ onClick={() => this.onDelete(portfolio._id)}
+ />
+ </div>
+ </div>
+ <ScenarioListContainer portfolioId={portfolio._id} />
+ </div>
+ ))}
+ </div>
+ )
+ }
+}
+
+export default PortfolioListComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js
new file mode 100644
index 00000000..4789315e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js
@@ -0,0 +1,15 @@
+import React from 'react'
+import Sidebar from '../Sidebar'
+import TopologyListContainer from '../../../../containers/app/sidebars/project/TopologyListContainer'
+import PortfolioListContainer from '../../../../containers/app/sidebars/project/PortfolioListContainer'
+
+const ProjectSidebarComponent = ({ collapsible }) => (
+ <Sidebar isRight={false} collapsible={collapsible}>
+ <div className="h-100 overflow-auto container-fluid">
+ <TopologyListContainer />
+ <PortfolioListContainer />
+ </div>
+ </Sidebar>
+)
+
+export default ProjectSidebarComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js
new file mode 100644
index 00000000..e775a663
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js
@@ -0,0 +1,62 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import Shapes from '../../../../shapes'
+import { Link } from 'react-router-dom'
+import FontAwesome from 'react-fontawesome'
+
+class ScenarioListComponent extends React.Component {
+ static propTypes = {
+ scenarios: PropTypes.arrayOf(Shapes.Scenario),
+ portfolioId: PropTypes.string,
+ currentProjectId: PropTypes.string.isRequired,
+ currentScenarioId: PropTypes.string,
+ onNewScenario: PropTypes.func.isRequired,
+ onChooseScenario: PropTypes.func.isRequired,
+ onDeleteScenario: PropTypes.func.isRequired,
+ }
+
+ onDelete(id) {
+ this.props.onDeleteScenario(id)
+ }
+
+ render() {
+ return (
+ <>
+ {this.props.scenarios.map((scenario, idx) => (
+ <div key={scenario._id} className="row mb-1">
+ <div
+ className={
+ 'col-7 pl-5 align-self-center ' +
+ (scenario._id === this.props.currentScenarioId ? 'font-weight-bold' : '')
+ }
+ >
+ {scenario.name}
+ </div>
+ <div className="col-5 text-right">
+ <Link
+ className="btn btn-outline-primary mr-1 fa fa-play disabled"
+ to={`/projects/${this.props.currentProjectId}/portfolios/${scenario.portfolioId}/scenarios/${scenario._id}`}
+ onClick={() => this.props.onChooseScenario(scenario.portfolioId, scenario._id)}
+ />
+ <span
+ className={'btn btn-outline-danger fa fa-trash ' + (idx === 0 ? 'disabled' : '')}
+ onClick={() => (idx !== 0 ? this.onDelete(scenario._id) : undefined)}
+ />
+ </div>
+ </div>
+ ))}
+ <div className="pl-4 mb-2">
+ <div
+ className="btn btn-outline-primary"
+ onClick={() => this.props.onNewScenario(this.props.portfolioId)}
+ >
+ <FontAwesome name="plus" className="mr-1" />
+ New scenario
+ </div>
+ </div>
+ </>
+ )
+ }
+}
+
+export default ScenarioListComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js
new file mode 100644
index 00000000..2f42f7e4
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js
@@ -0,0 +1,60 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import Shapes from '../../../../shapes'
+import FontAwesome from 'react-fontawesome'
+
+class TopologyListComponent extends React.Component {
+ static propTypes = {
+ topologies: PropTypes.arrayOf(Shapes.Topology),
+ currentTopologyId: PropTypes.string,
+ onChooseTopology: PropTypes.func.isRequired,
+ onNewTopology: PropTypes.func.isRequired,
+ onDeleteTopology: PropTypes.func.isRequired,
+ }
+
+ onChoose(id) {
+ this.props.onChooseTopology(id)
+ }
+
+ onDelete(id) {
+ this.props.onDeleteTopology(id)
+ }
+
+ render() {
+ return (
+ <div className="pb-3">
+ <h2>
+ Topologies
+ <button className="btn btn-outline-primary float-right" onClick={this.props.onNewTopology}>
+ <FontAwesome name="plus" />
+ </button>
+ </h2>
+
+ {this.props.topologies.map((topology, idx) => (
+ <div key={topology._id} className="row mb-1">
+ <div
+ className={
+ 'col-7 align-self-center ' +
+ (topology._id === this.props.currentTopologyId ? 'font-weight-bold' : '')
+ }
+ >
+ {topology.name}
+ </div>
+ <div className="col-5 text-right">
+ <span
+ className="btn btn-outline-primary mr-1 fa fa-play"
+ onClick={() => this.onChoose(topology._id)}
+ />
+ <span
+ className={'btn btn-outline-danger fa fa-trash ' + (idx === 0 ? 'disabled' : '')}
+ onClick={() => (idx !== 0 ? this.onDelete(topology._id) : undefined)}
+ />
+ </div>
+ </div>
+ ))}
+ </div>
+ )
+ }
+}
+
+export default TopologyListComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js
new file mode 100644
index 00000000..5fb0dc55
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import FontAwesome from 'react-fontawesome'
+
+const NameComponent = ({ name, onEdit }) => (
+ <h2>
+ {name}
+ <button className="btn btn-outline-secondary float-right" onClick={onEdit}>
+ <FontAwesome name="pencil" />
+ </button>
+ </h2>
+)
+
+export default NameComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js
new file mode 100644
index 00000000..f5eee36b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import BuildingSidebarContainer from '../../../../containers/app/sidebars/topology/building/BuildingSidebarContainer'
+import MachineSidebarContainer from '../../../../containers/app/sidebars/topology/machine/MachineSidebarContainer'
+import RackSidebarContainer from '../../../../containers/app/sidebars/topology/rack/RackSidebarContainer'
+import RoomSidebarContainer from '../../../../containers/app/sidebars/topology/room/RoomSidebarContainer'
+import Sidebar from '../Sidebar'
+
+const TopologySidebarComponent = ({ interactionLevel }) => {
+ let sidebarContent
+
+ switch (interactionLevel.mode) {
+ case 'BUILDING':
+ sidebarContent = <BuildingSidebarContainer />
+ break
+ case 'ROOM':
+ sidebarContent = <RoomSidebarContainer />
+ break
+ case 'RACK':
+ sidebarContent = <RackSidebarContainer />
+ break
+ case 'MACHINE':
+ sidebarContent = <MachineSidebarContainer />
+ break
+ default:
+ sidebarContent = 'Missing Content'
+ }
+
+ return <Sidebar isRight={true}>{sidebarContent}</Sidebar>
+}
+
+export default TopologySidebarComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js
new file mode 100644
index 00000000..eea62f84
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import NewRoomConstructionContainer from '../../../../../containers/app/sidebars/topology/building/NewRoomConstructionContainer'
+
+const BuildingSidebarComponent = () => {
+ return (
+ <div>
+ <h2>Building</h2>
+ <NewRoomConstructionContainer />
+ </div>
+ )
+}
+
+export default BuildingSidebarComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js
new file mode 100644
index 00000000..fd552c1e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js
@@ -0,0 +1,26 @@
+import React from 'react'
+
+const NewRoomConstructionComponent = ({ onStart, onFinish, onCancel, currentRoomInConstruction }) => {
+ if (currentRoomInConstruction === '-1') {
+ return (
+ <div className="btn btn-outline-primary btn-block" onClick={onStart}>
+ <span className="fa fa-plus mr-2" />
+ Construct a new room
+ </div>
+ )
+ }
+ return (
+ <div>
+ <div className="btn btn-primary btn-block" onClick={onFinish}>
+ <span className="fa fa-check mr-2" />
+ Finalize new room
+ </div>
+ <div className="btn btn-default btn-block" onClick={onCancel}>
+ <span className="fa fa-times mr-2" />
+ Cancel construction
+ </div>
+ </div>
+ )
+}
+
+export default NewRoomConstructionComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js
new file mode 100644
index 00000000..70d522b2
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js
@@ -0,0 +1,10 @@
+import React from 'react'
+
+const BackToRackComponent = ({ onClick }) => (
+ <div className="btn btn-secondary btn-block" onClick={onClick}>
+ <span className="fa fa-angle-left mr-2" />
+ Back to rack
+ </div>
+)
+
+export default BackToRackComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js
new file mode 100644
index 00000000..37820316
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js
@@ -0,0 +1,10 @@
+import React from 'react'
+
+const DeleteMachineComponent = ({ onClick }) => (
+ <div className="btn btn-outline-danger btn-block" onClick={onClick}>
+ <span className="fa fa-trash mr-2" />
+ Delete this machine
+ </div>
+)
+
+export default DeleteMachineComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineNameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineNameComponent.js
new file mode 100644
index 00000000..992383c4
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineNameComponent.js
@@ -0,0 +1,5 @@
+import React from 'react'
+
+const MachineNameComponent = ({ position }) => <h2>Machine at slot {position}</h2>
+
+export default MachineNameComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js
new file mode 100644
index 00000000..7c78cf9e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js
@@ -0,0 +1,18 @@
+import React from 'react'
+import BackToRackContainer from '../../../../../containers/app/sidebars/topology/machine/BackToRackContainer'
+import DeleteMachineContainer from '../../../../../containers/app/sidebars/topology/machine/DeleteMachineContainer'
+import MachineNameContainer from '../../../../../containers/app/sidebars/topology/machine/MachineNameContainer'
+import UnitTabsContainer from '../../../../../containers/app/sidebars/topology/machine/UnitTabsContainer'
+
+const MachineSidebarComponent = ({ machineId }) => {
+ return (
+ <div className="h-100 overflow-auto">
+ <MachineNameContainer />
+ <BackToRackContainer />
+ <DeleteMachineContainer />
+ <UnitTabsContainer />
+ </div>
+ )
+}
+
+export default MachineSidebarComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js
new file mode 100644
index 00000000..4e9dbc7e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js
@@ -0,0 +1,35 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+
+class UnitAddComponent extends React.Component {
+ static propTypes = {
+ units: PropTypes.array.isRequired,
+ onAdd: PropTypes.func.isRequired,
+ }
+
+ render() {
+ return (
+ <div className="form-inline">
+ <div className="form-group w-100">
+ <select className="form-control w-70 mr-1" ref={(unitSelect) => (this.unitSelect = unitSelect)}>
+ {this.props.units.map((unit) => (
+ <option value={unit._id} key={unit._id}>
+ {unit.name}
+ </option>
+ ))}
+ </select>
+ <button
+ type="submit"
+ className="btn btn-outline-primary"
+ onClick={() => this.props.onAdd(this.unitSelect.value)}
+ >
+ <span className="fa fa-plus mr-2" />
+ Add
+ </button>
+ </div>
+ </div>
+ )
+ }
+}
+
+export default UnitAddComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js
new file mode 100644
index 00000000..de55e506
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js
@@ -0,0 +1,52 @@
+import React from 'react'
+import { UncontrolledPopover, PopoverHeader, PopoverBody, Button } from 'reactstrap'
+
+function UnitComponent({ index, unitType, unit, onDelete }) {
+ let unitInfo
+ if (unitType === 'cpu' || unitType === 'gpu') {
+ unitInfo = (
+ <>
+ <strong>Clockrate: </strong>
+ <code>{unit.clockRateMhz}</code>
+ <br />
+ <strong>Num. Cores: </strong>
+ <code>{unit.numberOfCores}</code>
+ <br />
+ <strong>Energy Cons.: </strong>
+ <code>{unit.energyConsumptionW} W</code>
+ <br />
+ </>
+ )
+ } else if (unitType === 'memory' || unitType === 'storage') {
+ unitInfo = (
+ <>
+ <strong>Speed:</strong>
+ <code>{unit.speedMbPerS} Mb/s</code>
+ <br />
+ <strong>Size:</strong>
+ <code>{unit.sizeMb} MB</code>
+ <br />
+ <strong>Energy Cons.:</strong>
+ <code>{unit.energyConsumptionW} W</code>
+ <br />
+ </>
+ )
+ }
+
+ return (
+ <li className="d-flex list-group-item justify-content-between align-items-center">
+ <span style={{ maxWidth: '60%' }}>{unit.name}</span>
+ <span>
+ <Button outline={true} color="info" className="mr-1 fa fa-info-circle" id={`unit-${index}`} />
+ <UncontrolledPopover trigger="focus" placement="left" target={`unit-${index}`}>
+ <PopoverHeader>Unit Information</PopoverHeader>
+ <PopoverBody>{unitInfo}</PopoverBody>
+ </UncontrolledPopover>
+
+ <span className="btn btn-outline-danger fa fa-trash" onClick={onDelete} />
+ </span>
+ </li>
+ )
+}
+
+export default UnitComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js
new file mode 100644
index 00000000..2ade0f6a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import UnitContainer from '../../../../../containers/app/sidebars/topology/machine/UnitContainer'
+
+const UnitListComponent = ({ unitType, unitIds }) => (
+ <ul className="list-group mt-1">
+ {unitIds.length !== 0 ? (
+ unitIds.map((unitId, index) => (
+ <UnitContainer unitType={unitType} unitId={unitId} index={index} key={index} />
+ ))
+ ) : (
+ <div className="alert alert-info">
+ <span>
+ <strong>No units...</strong> Add some with the menu above!
+ </span>
+ </div>
+ )}
+ </ul>
+)
+
+export default UnitListComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js
new file mode 100644
index 00000000..6599fefd
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js
@@ -0,0 +1,78 @@
+import React, { useState } from 'react'
+import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap'
+import UnitAddContainer from '../../../../../containers/app/sidebars/topology/machine/UnitAddContainer'
+import UnitListContainer from '../../../../../containers/app/sidebars/topology/machine/UnitListContainer'
+
+const UnitTabsComponent = () => {
+ const [activeTab, setActiveTab] = useState('cpu-units')
+ const toggle = (tab) => {
+ if (activeTab !== tab) setActiveTab(tab)
+ }
+
+ return (
+ <div>
+ <Nav tabs>
+ <NavItem>
+ <NavLink
+ className={activeTab === 'cpu-units' ? 'active' : ''}
+ onClick={() => {
+ toggle('cpu-units')
+ }}
+ >
+ CPU
+ </NavLink>
+ </NavItem>
+ <NavItem>
+ <NavLink
+ className={activeTab === 'gpu-units' ? 'active' : ''}
+ onClick={() => {
+ toggle('gpu-units')
+ }}
+ >
+ GPU
+ </NavLink>
+ </NavItem>
+ <NavItem>
+ <NavLink
+ className={activeTab === 'memory-units' ? 'active' : ''}
+ onClick={() => {
+ toggle('memory-units')
+ }}
+ >
+ Memory
+ </NavLink>
+ </NavItem>
+ <NavItem>
+ <NavLink
+ className={activeTab === 'storage-units' ? 'active' : ''}
+ onClick={() => {
+ toggle('storage-units')
+ }}
+ >
+ Stor.
+ </NavLink>
+ </NavItem>
+ </Nav>
+ <TabContent activeTab={activeTab}>
+ <TabPane tabId="cpu-units">
+ <UnitAddContainer unitType="cpu" />
+ <UnitListContainer unitType="cpu" />
+ </TabPane>
+ <TabPane tabId="gpu-units">
+ <UnitAddContainer unitType="gpu" />
+ <UnitListContainer unitType="gpu" />
+ </TabPane>
+ <TabPane tabId="memory-units">
+ <UnitAddContainer unitType="memory" />
+ <UnitListContainer unitType="memory" />
+ </TabPane>
+ <TabPane tabId="storage-units">
+ <UnitAddContainer unitType="storage" />
+ <UnitListContainer unitType="storage" />
+ </TabPane>
+ </TabContent>
+ </div>
+ )
+}
+
+export default UnitTabsComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js
new file mode 100644
index 00000000..75418f9d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js
@@ -0,0 +1,10 @@
+import React from 'react'
+
+const AddPrefabComponent = ({ onClick }) => (
+ <div className="btn btn-primary btn-block" onClick={onClick}>
+ <span className="fa fa-floppy-o mr-2" />
+ Save this rack to a prefab
+ </div>
+)
+
+export default AddPrefabComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js
new file mode 100644
index 00000000..c14775bf
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js
@@ -0,0 +1,10 @@
+import React from 'react'
+
+const BackToRoomComponent = ({ onClick }) => (
+ <div className="btn btn-secondary btn-block mb-2" onClick={onClick}>
+ <span className="fa fa-angle-left mr-2" />
+ Back to room
+ </div>
+)
+
+export default BackToRoomComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackComponent.js
new file mode 100644
index 00000000..23b0daac
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackComponent.js
@@ -0,0 +1,10 @@
+import React from 'react'
+
+const DeleteRackComponent = ({ onClick }) => (
+ <div className="btn btn-outline-danger btn-block" onClick={onClick}>
+ <span className="fa fa-trash mr-2" />
+ Delete this rack
+ </div>
+)
+
+export default DeleteRackComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js
new file mode 100644
index 00000000..d7e30f1d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js
@@ -0,0 +1,13 @@
+import React from 'react'
+
+const EmptySlotComponent = ({ position, onAdd }) => (
+ <li className="list-group-item d-flex justify-content-between align-items-center">
+ <span className="badge badge-default badge-info mr-1 disabled">{position}</span>
+ <button className="btn btn-outline-primary" onClick={onAdd}>
+ <span className="fa fa-plus mr-2" />
+ Add machine
+ </button>
+ </li>
+)
+
+export default EmptySlotComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js
new file mode 100644
index 00000000..caa3dc04
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js
@@ -0,0 +1,43 @@
+import React from 'react'
+import Shapes from '../../../../../shapes'
+
+const UnitIcon = ({ id, type }) => (
+ <div>
+ <img
+ src={'/img/topology/' + id + '-icon.png'}
+ alt={'Machine contains ' + type + ' units'}
+ className="img-fluid ml-1"
+ style={{ maxHeight: '35px' }}
+ />
+ </div>
+)
+
+const MachineComponent = ({ position, machine, onClick }) => {
+ const hasNoUnits =
+ machine.cpuIds.length + machine.gpuIds.length + machine.memoryIds.length + machine.storageIds.length === 0
+
+ return (
+ <li
+ className="d-flex list-group-item list-group-item-action justify-content-between align-items-center"
+ onClick={onClick}
+ style={{ backgroundColor: 'white' }}
+ >
+ <span className="badge badge-default badge-info mr-1">{position}</span>
+ <div className="d-inline-flex">
+ {machine.cpuIds.length > 0 ? <UnitIcon id="cpu" type="CPU" /> : undefined}
+ {machine.gpuIds.length > 0 ? <UnitIcon id="gpu" type="GPU" /> : undefined}
+ {machine.memoryIds.length > 0 ? <UnitIcon id="memory" type="memory" /> : undefined}
+ {machine.storageIds.length > 0 ? <UnitIcon id="storage" type="storage" /> : undefined}
+ {hasNoUnits ? (
+ <span className="badge badge-default badge-warning">Machine with no units</span>
+ ) : undefined}
+ </div>
+ </li>
+ )
+}
+
+MachineComponent.propTypes = {
+ machine: Shapes.Machine,
+}
+
+export default MachineComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js
new file mode 100644
index 00000000..12be26bd
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import EmptySlotContainer from '../../../../../containers/app/sidebars/topology/rack/EmptySlotContainer'
+import MachineContainer from '../../../../../containers/app/sidebars/topology/rack/MachineContainer'
+import './MachineListComponent.sass'
+
+const MachineListComponent = ({ machineIds }) => {
+ return (
+ <ul className="list-group machine-list">
+ {machineIds.map((machineId, index) => {
+ if (machineId === null) {
+ return <EmptySlotContainer key={index} position={index + 1} />
+ } else {
+ return <MachineContainer key={index} position={index + 1} machineId={machineId} />
+ }
+ })}
+ </ul>
+ )
+}
+
+export default MachineListComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.sass b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.sass
new file mode 100644
index 00000000..11b82c93
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.sass
@@ -0,0 +1,2 @@
+.machine-list li
+ min-height: 64px
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameComponent.js
new file mode 100644
index 00000000..b701909a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameComponent.js
@@ -0,0 +1,6 @@
+import React from 'react'
+import NameComponent from '../NameComponent'
+
+const RackNameComponent = ({ rackName, onEdit }) => <NameComponent name={rackName} onEdit={onEdit} />
+
+export default RackNameComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js
new file mode 100644
index 00000000..ca41bf57
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import BackToRoomContainer from '../../../../../containers/app/sidebars/topology/rack/BackToRoomContainer'
+import DeleteRackContainer from '../../../../../containers/app/sidebars/topology/rack/DeleteRackContainer'
+import MachineListContainer from '../../../../../containers/app/sidebars/topology/rack/MachineListContainer'
+import RackNameContainer from '../../../../../containers/app/sidebars/topology/rack/RackNameContainer'
+import './RackSidebarComponent.sass'
+import AddPrefabContainer from '../../../../../containers/app/sidebars/topology/rack/AddPrefabContainer'
+
+const RackSidebarComponent = () => {
+ return (
+ <div className="rack-sidebar-container flex-column">
+ <div className="rack-sidebar-header-container">
+ <RackNameContainer />
+ <BackToRoomContainer />
+ <AddPrefabContainer />
+ <DeleteRackContainer />
+ </div>
+ <div className="machine-list-container mt-2">
+ <MachineListContainer />
+ </div>
+ </div>
+ )
+}
+
+export default RackSidebarComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass
new file mode 100644
index 00000000..29fec02a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass
@@ -0,0 +1,11 @@
+.rack-sidebar-container
+ display: flex
+ height: 100%
+ max-height: 100%
+
+.rack-sidebar-header-container
+ flex: 0
+
+.machine-list-container
+ flex: 1
+ overflow-y: scroll
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js
new file mode 100644
index 00000000..64c0a1f6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js
@@ -0,0 +1,10 @@
+import React from 'react'
+
+const BackToBuildingComponent = ({ onClick }) => (
+ <div className="btn btn-secondary btn-block mb-2" onClick={onClick}>
+ <span className="fa fa-angle-left mr-2" />
+ Back to building
+ </div>
+)
+
+export default BackToBuildingComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js
new file mode 100644
index 00000000..78417359
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js
@@ -0,0 +1,10 @@
+import React from 'react'
+
+const DeleteRoomComponent = ({ onClick }) => (
+ <div className="btn btn-outline-danger btn-block" onClick={onClick}>
+ <span className="fa fa-trash mr-2" />
+ Delete this room
+ </div>
+)
+
+export default DeleteRoomComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomComponent.js
new file mode 100644
index 00000000..857a646f
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomComponent.js
@@ -0,0 +1,22 @@
+import classNames from 'classnames'
+import React from 'react'
+
+const EditRoomComponent = ({ onEdit, onFinish, isEditing, isInRackConstructionMode }) =>
+ isEditing ? (
+ <div className="btn btn-info btn-block" onClick={onFinish}>
+ <span className="fa fa-check mr-2" />
+ Finish editing room
+ </div>
+ ) : (
+ <div
+ className={classNames('btn btn-outline-info btn-block', {
+ disabled: isInRackConstructionMode,
+ })}
+ onClick={() => (isInRackConstructionMode ? undefined : onEdit())}
+ >
+ <span className="fa fa-pencil mr-2" />
+ Edit the tiles of this room
+ </div>
+ )
+
+export default EditRoomComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js
new file mode 100644
index 00000000..44566f61
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js
@@ -0,0 +1,27 @@
+import classNames from 'classnames'
+import React from 'react'
+
+const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, isEditingRoom }) => {
+ if (inRackConstructionMode) {
+ return (
+ <div className="btn btn-primary btn-block" onClick={onStop}>
+ <span className="fa fa-times mr-2" />
+ Stop rack construction
+ </div>
+ )
+ }
+
+ return (
+ <div
+ className={classNames('btn btn-outline-primary btn-block', {
+ disabled: isEditingRoom,
+ })}
+ onClick={() => (isEditingRoom ? undefined : onStart())}
+ >
+ <span className="fa fa-plus mr-2" />
+ Start rack construction
+ </div>
+ )
+}
+
+export default RackConstructionComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomNameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomNameComponent.js
new file mode 100644
index 00000000..d637828e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomNameComponent.js
@@ -0,0 +1,6 @@
+import React from 'react'
+import NameComponent from '../NameComponent'
+
+const RoomNameComponent = ({ roomName, onEdit }) => <NameComponent name={roomName} onEdit={onEdit} />
+
+export default RoomNameComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js
new file mode 100644
index 00000000..1bc6533e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import BackToBuildingContainer from '../../../../../containers/app/sidebars/topology/room/BackToBuildingContainer'
+import DeleteRoomContainer from '../../../../../containers/app/sidebars/topology/room/DeleteRoomContainer'
+import EditRoomContainer from '../../../../../containers/app/sidebars/topology/room/EditRoomContainer'
+import RackConstructionContainer from '../../../../../containers/app/sidebars/topology/room/RackConstructionContainer'
+import RoomNameContainer from '../../../../../containers/app/sidebars/topology/room/RoomNameContainer'
+
+const RoomSidebarComponent = () => {
+ return (
+ <div>
+ <RoomNameContainer />
+ <BackToBuildingContainer />
+ <RackConstructionContainer />
+ <EditRoomContainer />
+ <DeleteRoomContainer />
+ </div>
+ )
+}
+
+export default RoomSidebarComponent
diff --git a/opendc-web/opendc-web-ui/src/components/home/ContactSection.js b/opendc-web/opendc-web-ui/src/components/home/ContactSection.js
new file mode 100644
index 00000000..42bdab8a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/ContactSection.js
@@ -0,0 +1,54 @@
+import React from 'react'
+import FontAwesome from 'react-fontawesome'
+import './ContactSection.sass'
+import ContentSection from './ContentSection'
+
+const ContactSection = () => (
+ <ContentSection name="contact" title="Contact">
+ <div className="row justify-content-center">
+ <div className="col-4">
+ <a href="https://github.com/atlarge-research/opendc">
+ <FontAwesome name="github" size="3x" className="mb-2" />
+ <div className="w-100" />
+ atlarge-research/opendc
+ </a>
+ </div>
+ <div className="col-4">
+ <a href="mailto:opendc@atlarge-research.com">
+ <FontAwesome name="envelope" size="3x" className="mb-2" />
+ <div className="w-100" />
+ opendc@atlarge-research.com
+ </a>
+ </div>
+ </div>
+ <div className="row">
+ <div className="col text-center">
+ <img src="img/tudelft-icon.png" className="img-fluid tudelft-icon" alt="TU Delft" />
+ </div>
+ </div>
+ <div className="row">
+ <div className="col text-center">
+ A project by the &nbsp;
+ <a href="http://atlarge.science" target="_blank" rel="noopener noreferrer">
+ <strong>@Large Research Group</strong>
+ </a>
+ .
+ </div>
+ </div>
+ <div className="row">
+ <div className="col text-center disclaimer mt-5 small">
+ <FontAwesome name="exclamation-triangle" size="2x" className="mr-2" />
+ <br />
+ OpenDC is an experimental tool. Your data may get lost, overwritten, or otherwise become unavailable.
+ <br />
+ The OpenDC authors should in no way be liable in the event this happens (see our{' '}
+ <strong>
+ <a href="https://github.com/atlarge-research/opendc/blob/master/LICENSE.md">license</a>
+ </strong>
+ ). Sorry for the inconvenience.
+ </div>
+ </div>
+ </ContentSection>
+)
+
+export default ContactSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/ContactSection.sass b/opendc-web/opendc-web-ui/src/components/home/ContactSection.sass
new file mode 100644
index 00000000..997f8d98
--- /dev/null
+++ b/opendc-web/opendc-web-ui/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/opendc-web/opendc-web-ui/src/components/home/ContentSection.js b/opendc-web/opendc-web-ui/src/components/home/ContentSection.js
new file mode 100644
index 00000000..9d4832d9
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/ContentSection.js
@@ -0,0 +1,19 @@
+import classNames from 'classnames'
+import PropTypes from 'prop-types'
+import React from 'react'
+import './ContentSection.sass'
+
+const ContentSection = ({ name, title, children }) => (
+ <div id={name} className={classNames(name + '-section', 'content-section')}>
+ <div className="container">
+ <h1>{title}</h1>
+ {children}
+ </div>
+ </div>
+)
+
+ContentSection.propTypes = {
+ name: PropTypes.string.isRequired,
+}
+
+export default ContentSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/ContentSection.sass b/opendc-web/opendc-web-ui/src/components/home/ContentSection.sass
new file mode 100644
index 00000000..a4c8bd66
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/ContentSection.sass
@@ -0,0 +1,9 @@
+@import ../../style-globals/_variables.sass
+
+.content-section
+ padding-top: 50px
+ padding-bottom: 150px
+ text-align: center
+
+ h1
+ margin-bottom: 30px
diff --git a/opendc-web/opendc-web-ui/src/components/home/IntroSection.js b/opendc-web/opendc-web-ui/src/components/home/IntroSection.js
new file mode 100644
index 00000000..a799272a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/IntroSection.js
@@ -0,0 +1,40 @@
+import React from 'react'
+
+const IntroSection = () => (
+ <section id="intro" className="intro-section">
+ <div className="container pt-5 pb-3">
+ <div className="row justify-content-center">
+ <div className="col-xl-4 col-lg-4 col-md-4 col-sm-8 col-8">
+ <h4>The datacenter (DC) industry...</h4>
+ <ul>
+ <li>Is worth over $15 bn, and growing</li>
+ <li>Has many hard-to-grasp concepts</li>
+ <li>Needs to become accessible to many</li>
+ </ul>
+ </div>
+ <div className="col-xl-4 col-lg-4 col-md-4 col-sm-8 col-8">
+ <img
+ src="img/datacenter-drawing.png"
+ className="col-12 img-fluid"
+ alt="Schematic top-down view of a datacenter"
+ />
+ <p className="col-12 figure-caption text-center">
+ <a href="http://www.dolphinhosts.co.uk/wp-content/uploads/2013/07/data-centers.gif">
+ Image source
+ </a>
+ </p>
+ </div>
+ <div className="col-xl-4 col-lg-4 col-md-4 col-sm-8 col-8">
+ <h4>OpenDC provides...</h4>
+ <ul>
+ <li>Collaborative online DC modeling</li>
+ <li>Diverse and effective DC simulation</li>
+ <li>Exploratory DC performance feedback</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </section>
+)
+
+export default IntroSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.js b/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.js
new file mode 100644
index 00000000..7b410679
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.js
@@ -0,0 +1,18 @@
+import React from 'react'
+import './JumbotronHeader.sass'
+
+const JumbotronHeader = () => (
+ <section className="jumbotron-header">
+ <div className="container">
+ <div className="jumbotron text-center">
+ <h1>
+ Open<span className="dc">DC</span>
+ </h1>
+ <p className="lead">Collaborative Datacenter Simulation and Exploration for Everybody</p>
+ <img src="img/logo.png" className="img-responsive mt-3" alt="OpenDC" />
+ </div>
+ </div>
+ </section>
+)
+
+export default JumbotronHeader
diff --git a/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.sass b/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.sass
new file mode 100644
index 00000000..1b6a89fd
--- /dev/null
+++ b/opendc-web/opendc-web-ui/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/opendc-web/opendc-web-ui/src/components/home/ModelingSection.js b/opendc-web/opendc-web-ui/src/components/home/ModelingSection.js
new file mode 100644
index 00000000..643dca65
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/ModelingSection.js
@@ -0,0 +1,22 @@
+import React from 'react'
+import ScreenshotSection from './ScreenshotSection'
+
+const ModelingSection = () => (
+ <ScreenshotSection
+ name="modeling"
+ title="Datacenter Modeling"
+ imageUrl="/img/screenshot-construction.png"
+ caption="Building a datacenter in OpenDC"
+ imageIsRight={true}
+ >
+ <h3>Collaboratively...</h3>
+ <ul>
+ <li>Model DC layout, and room locations and types</li>
+ <li>Place racks in rooms and nodes in racks</li>
+ <li>Add real-world CPU, GPU, memory, storage and network units to each node</li>
+ <li>Select from diverse scheduling policies</li>
+ </ul>
+ </ScreenshotSection>
+)
+
+export default ModelingSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.js b/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.js
new file mode 100644
index 00000000..c987d5d0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.js
@@ -0,0 +1,24 @@
+import classNames from 'classnames'
+import React from 'react'
+import ContentSection from './ContentSection'
+import './ScreenshotSection.sass'
+
+const ScreenshotSection = ({ name, title, imageUrl, caption, imageIsRight, children }) => (
+ <ContentSection name={name} title={title}>
+ <div className="row">
+ <div
+ className={classNames('col-xl-5 col-lg-5 col-md-5 col-sm-12 col-12 text-left', {
+ 'order-1': !imageIsRight,
+ })}
+ >
+ {children}
+ </div>
+ <div className="col-xl-7 col-lg-7 col-md-7 col-sm-12 col-12">
+ <img src={imageUrl} className="col-12 screenshot" alt={caption} />
+ <div className="row text-muted justify-content-center">{caption}</div>
+ </div>
+ </div>
+ </ContentSection>
+)
+
+export default ScreenshotSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.sass b/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.sass
new file mode 100644
index 00000000..2f454cb4
--- /dev/null
+++ b/opendc-web/opendc-web-ui/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/opendc-web/opendc-web-ui/src/components/home/SimulationSection.js b/opendc-web/opendc-web-ui/src/components/home/SimulationSection.js
new file mode 100644
index 00000000..b0244cb5
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/SimulationSection.js
@@ -0,0 +1,22 @@
+import React from 'react'
+import ScreenshotSection from './ScreenshotSection'
+
+const ModelingSection = () => (
+ <ScreenshotSection
+ name="project"
+ title="Datacenter Simulation"
+ imageUrl="/img/screenshot-simulation-zoom.png"
+ caption="Running an experiment in OpenDC"
+ imageIsRight={false}
+ >
+ <h3>Working with OpenDC:</h3>
+ <ul>
+ <li>Seamlessly switch between construction and simulation modes</li>
+ <li>Choose one of several predefined workloads (Big Data, Bag of Tasks, Hadoop, etc.)</li>
+ <li>Play, pause, and skip around the informative simulation timeline</li>
+ <li>Visualize and demo live</li>
+ </ul>
+ </ScreenshotSection>
+)
+
+export default ModelingSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js b/opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js
new file mode 100644
index 00000000..e5ed9683
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js
@@ -0,0 +1,30 @@
+import React from 'react'
+import ContentSection from './ContentSection'
+
+const Stakeholder = ({ name, title, subtitle }) => (
+ <div className="col-xl-4 col-lg-4 col-md-4 col-sm-6 col-6">
+ <img
+ src={'img/stakeholders/' + name + '.png'}
+ className="col-xl-3 col-lg-4 col-md-4 col-sm-4 col-4 img-fluid"
+ alt={title}
+ />
+ <div className="text-center mt-2">
+ <h4>{title}</h4>
+ <p>{subtitle}</p>
+ </div>
+ </div>
+)
+
+const StakeholderSection = () => (
+ <ContentSection name="stakeholders" title="Stakeholders">
+ <div className="row justify-content-center">
+ <Stakeholder name="Manager" title="Managers" subtitle="Seeing is deciding" />
+ <Stakeholder name="Sales" title="Sales" subtitle="Demo concepts" />
+ <Stakeholder name="Developer" title="DevOps" subtitle="Develop & tune" />
+ <Stakeholder name="Researcher" title="Researchers" subtitle="Understand & design" />
+ <Stakeholder name="Student" title="Students" subtitle="Grasp complex concepts" />
+ </div>
+ </ContentSection>
+)
+
+export default StakeholderSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/TeamSection.js b/opendc-web/opendc-web-ui/src/components/home/TeamSection.js
new file mode 100644
index 00000000..4b6f1e25
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/TeamSection.js
@@ -0,0 +1,53 @@
+import React from 'react'
+import ContentSection from './ContentSection'
+
+const TeamMember = ({ photoId, name, description }) => (
+ <div className="col-xl-4 col-lg-4 col-md-5 col-sm-6 col-12 justify-content-center">
+ <img
+ src={'img/portraits/' + photoId + '.png'}
+ className="col-xl-10 col-lg-10 col-md-10 col-sm-8 col-5 mb-2 mt-2"
+ alt={name}
+ />
+ <div className="col-12">
+ <h4>{name}</h4>
+ <div className="team-member-description">{description}</div>
+ </div>
+ </div>
+)
+
+const TeamSection = () => (
+ <ContentSection name="team" title="Core Team">
+ <div className="row justify-content-center">
+ <TeamMember photoId="aiosup" name="Prof. dr. ir. Alexandru Iosup" description="Project Lead" />
+ <TeamMember
+ photoId="gandreadis"
+ name="Georgios Andreadis"
+ description="Software Engineer responsible for the frontend web application"
+ />
+ <TeamMember
+ photoId="fmastenbroek"
+ name="Fabian Mastenbroek"
+ description="Software Engineer responsible for the datacenter simulator"
+ />
+ <TeamMember
+ photoId="jburley"
+ name="Jacob Burley"
+ description="Software Engineer responsible for prefabricated components"
+ />
+ <TeamMember
+ photoId="loverweel"
+ name="Leon Overweel"
+ description="Former product lead and Software Engineer"
+ />
+ </div>
+ <div className="text-center lead mt-3">
+ See{' '}
+ <a target="_blank" href="http://atlarge.science/opendc#team" rel="noopener noreferrer">
+ atlarge.science/opendc
+ </a>{' '}
+ for the full team!
+ </div>
+ </ContentSection>
+)
+
+export default TeamSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/TechnologiesSection.js b/opendc-web/opendc-web-ui/src/components/home/TechnologiesSection.js
new file mode 100644
index 00000000..c6013c71
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/home/TechnologiesSection.js
@@ -0,0 +1,40 @@
+import React from 'react'
+import FontAwesome from 'react-fontawesome'
+import ContentSection from './ContentSection'
+
+const TechnologiesSection = () => (
+ <ContentSection name="technologies" title="Technologies">
+ <ul className="list-group text-left">
+ <li className="d-flex list-group-item justify-content-between align-items-center list-group-item-primary">
+ <span style={{ minWidth: 100 }}>
+ <FontAwesome name="window-maximize" className="mr-2" />
+ <strong className="">Browser</strong>
+ </span>
+ <span className="text-right">JavaScript, React, Redux, Konva</span>
+ </li>
+ <li className="d-flex list-group-item justify-content-between align-items-center list-group-item-warning">
+ <span style={{ minWidth: 100 }}>
+ <FontAwesome name="television" className="mr-2" />
+ <strong>Server</strong>
+ </span>
+ <span className="text-right">Python, Flask, FlaskSocketIO, OpenAPI</span>
+ </li>
+ <li className="d-flex list-group-item justify-content-between align-items-center list-group-item-success">
+ <span style={{ minWidth: 100 }}>
+ <FontAwesome name="database" className="mr-2" />
+ <strong>Database</strong>
+ </span>
+ <span className="text-right">MongoDB</span>
+ </li>
+ <li className="d-flex list-group-item justify-content-between align-items-center list-group-item-danger">
+ <span style={{ minWidth: 100 }}>
+ <FontAwesome name="cogs" className="mr-2" />
+ <strong>Simulator</strong>
+ </span>
+ <span className="text-right">Kotlin</span>
+ </li>
+ </ul>
+ </ContentSection>
+)
+
+export default TechnologiesSection
diff --git a/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js b/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js
new file mode 100644
index 00000000..589047dc
--- /dev/null
+++ b/opendc-web/opendc-web-ui/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 (
+ <Modal
+ title={this.props.title}
+ show={this.props.show}
+ onSubmit={this.onConfirm.bind(this)}
+ onCancel={this.onCancel.bind(this)}
+ submitButtonType="danger"
+ submitButtonText="Confirm"
+ >
+ {this.props.message}
+ </Modal>
+ )
+ }
+}
+
+export default ConfirmationModal
diff --git a/opendc-web/opendc-web-ui/src/components/modals/Modal.js b/opendc-web/opendc-web-ui/src/components/modals/Modal.js
new file mode 100644
index 00000000..21b7f119
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/modals/Modal.js
@@ -0,0 +1,53 @@
+import React, { useState, useEffect } from 'react'
+import PropTypes from 'prop-types'
+import { Modal as RModal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'
+
+function Modal({ children, title, show, onSubmit, onCancel, submitButtonType, submitButtonText }) {
+ const [modal, setModal] = useState(show)
+
+ useEffect(() => setModal(show), [show])
+
+ const toggle = () => setModal(!modal)
+ const cancel = () => {
+ if (onCancel() !== false) {
+ toggle()
+ }
+ }
+ const submit = () => {
+ if (onSubmit() !== false) {
+ toggle()
+ }
+ }
+
+ return (
+ <RModal isOpen={modal} toggle={cancel}>
+ <ModalHeader toggle={cancel}>{title}</ModalHeader>
+ <ModalBody>{children}</ModalBody>
+ <ModalFooter>
+ <Button color="secondary" onClick={cancel}>
+ Close
+ </Button>
+ <Button color={submitButtonType} onClick={submit}>
+ {submitButtonText}
+ </Button>
+ </ModalFooter>
+ </RModal>
+ )
+}
+
+Modal.propTypes = {
+ title: PropTypes.string.isRequired,
+ show: PropTypes.bool.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+ submitButtonType: PropTypes.string,
+ submitButtonText: PropTypes.string,
+}
+
+Modal.defaultProps = {
+ submitButtonType: 'primary',
+ submitButtonText: 'Save',
+ show: false,
+}
+
+export default Modal
diff --git a/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js b/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js
new file mode 100644
index 00000000..d0918c7e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js
@@ -0,0 +1,54 @@
+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) {
+ 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 (
+ <Modal
+ title={this.props.title}
+ show={this.props.show}
+ onSubmit={this.onSubmit.bind(this)}
+ onCancel={this.onCancel.bind(this)}
+ >
+ <form
+ onSubmit={(e) => {
+ e.preventDefault()
+ this.onSubmit()
+ }}
+ >
+ <div className="form-group">
+ <label className="form-control-label">{this.props.label}</label>
+ <input type="text" className="form-control" ref={(textInput) => (this.textInput = textInput)} />
+ </div>
+ </form>
+ </Modal>
+ )
+ }
+}
+
+export default TextInputModal
diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js
new file mode 100644
index 00000000..3c6b8724
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js
@@ -0,0 +1,78 @@
+import PropTypes from 'prop-types'
+import React, { useRef } from 'react'
+import { Form, FormGroup, Input, Label } from 'reactstrap'
+import Modal from '../Modal'
+import { AVAILABLE_METRICS, METRIC_NAMES } from '../../../util/available-metrics'
+
+const NewPortfolioModalComponent = ({ show, callback }) => {
+ const form = useRef(null)
+ const textInput = useRef(null)
+ const repeatsInput = useRef(null)
+ const metricCheckboxes = useRef({})
+
+ const onSubmit = () => {
+ if (form.current.reportValidity()) {
+ callback(textInput.current.value, {
+ enabledMetrics: AVAILABLE_METRICS.filter((metric) => metricCheckboxes.current[metric].checked),
+ repeatsPerScenario: parseInt(repeatsInput.current.value),
+ })
+
+ return true
+ } else {
+ return false
+ }
+ }
+ const onCancel = () => callback(undefined)
+
+ return (
+ <Modal title="New Portfolio" show={show} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form
+ onSubmit={(e) => {
+ e.preventDefault()
+ this.onSubmit()
+ }}
+ innerRef={form}
+ >
+ <FormGroup>
+ <Label for="name">Name</Label>
+ <Input name="name" type="text" required innerRef={textInput} placeholder="My Portfolio" />
+ </FormGroup>
+ <h4>Targets</h4>
+ <h5>Metrics</h5>
+ <FormGroup>
+ {AVAILABLE_METRICS.map((metric) => (
+ <FormGroup check key={metric}>
+ <Label for={metric} check>
+ <Input
+ name={metric}
+ type="checkbox"
+ innerRef={(ref) => (metricCheckboxes.current[metric] = ref)}
+ />
+ {METRIC_NAMES[metric]}
+ </Label>
+ </FormGroup>
+ ))}
+ </FormGroup>
+ <FormGroup>
+ <Label for="repeats">Repeats per scenario</Label>
+ <Input
+ name="repeats"
+ type="number"
+ required
+ innerRef={repeatsInput}
+ defaultValue="1"
+ min="1"
+ step="1"
+ />
+ </FormGroup>
+ </Form>
+ </Modal>
+ )
+}
+
+NewPortfolioModalComponent.propTypes = {
+ show: PropTypes.bool.isRequired,
+ callback: PropTypes.func.isRequired,
+}
+
+export default NewPortfolioModalComponent
diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js
new file mode 100644
index 00000000..01a5719c
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js
@@ -0,0 +1,144 @@
+import PropTypes from 'prop-types'
+import React, { useRef } from 'react'
+import { Form, FormGroup, Input, Label } from 'reactstrap'
+import Shapes from '../../../shapes'
+import Modal from '../Modal'
+
+const NewScenarioModalComponent = ({
+ show,
+ callback,
+ currentPortfolioId,
+ currentPortfolioScenarioIds,
+ traces,
+ topologies,
+ schedulers,
+}) => {
+ const form = useRef(null)
+ const textInput = useRef(null)
+ const traceSelect = useRef(null)
+ const traceLoadInput = useRef(null)
+ const topologySelect = useRef(null)
+ const failuresCheckbox = useRef(null)
+ const performanceInterferenceCheckbox = useRef(null)
+ const schedulerSelect = useRef(null)
+
+ const onSubmit = () => {
+ if (!form.current.reportValidity()) {
+ return false
+ }
+ callback(
+ textInput.current.value,
+ currentPortfolioId,
+ {
+ traceId: traceSelect.current.value,
+ loadSamplingFraction: parseFloat(traceLoadInput.current.value),
+ },
+ {
+ topologyId: topologySelect.current.value,
+ },
+ {
+ failuresEnabled: failuresCheckbox.current.checked,
+ performanceInterferenceEnabled: performanceInterferenceCheckbox.current.checked,
+ schedulerName: schedulerSelect.current.value,
+ }
+ )
+ return true
+ }
+ const onCancel = () => {
+ callback(undefined)
+ }
+
+ return (
+ <Modal title="New Scenario" show={show} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form
+ onSubmit={(e) => {
+ e.preventDefault()
+ onSubmit()
+ }}
+ innerRef={form}
+ >
+ <FormGroup>
+ <Label for="name">Name</Label>
+ <Input
+ name="name"
+ type="text"
+ required
+ disabled={currentPortfolioScenarioIds.length === 0}
+ defaultValue={currentPortfolioScenarioIds.length === 0 ? 'Base scenario' : ''}
+ innerRef={textInput}
+ />
+ </FormGroup>
+ <h4>Trace</h4>
+ <FormGroup>
+ <Label for="trace">Trace</Label>
+ <Input name="trace" type="select" innerRef={traceSelect}>
+ {traces.map((trace) => (
+ <option value={trace._id} key={trace._id}>
+ {trace.name}
+ </option>
+ ))}
+ </Input>
+ </FormGroup>
+ <FormGroup>
+ <Label for="trace-load">Load sampling fraction</Label>
+ <Input
+ name="trace-load"
+ type="number"
+ innerRef={traceLoadInput}
+ required
+ defaultValue="1"
+ min="0"
+ max="1"
+ step="0.1"
+ />
+ </FormGroup>
+ <h4>Topology</h4>
+ <div className="form-group">
+ <Label for="topology">Topology</Label>
+ <Input name="topology" type="select" innerRef={topologySelect}>
+ {topologies.map((topology) => (
+ <option value={topology._id} key={topology._id}>
+ {topology.name}
+ </option>
+ ))}
+ </Input>
+ </div>
+ <h4>Operational Phenomena</h4>
+ <FormGroup check>
+ <Label check for="failures">
+ <Input type="checkbox" name="failures" innerRef={failuresCheckbox} />{' '}
+ <span className="ml-2">Enable failures</span>
+ </Label>
+ </FormGroup>
+ <FormGroup check>
+ <Label check for="perf-interference">
+ <Input type="checkbox" name="perf-interference" innerRef={performanceInterferenceCheckbox} />{' '}
+ <span className="ml-2">Enable performance interference</span>
+ </Label>
+ </FormGroup>
+ <FormGroup>
+ <Label for="scheduler">Scheduler</Label>
+ <Input name="scheduler" type="select" innerRef={schedulerSelect}>
+ {schedulers.map((scheduler) => (
+ <option value={scheduler.name} key={scheduler.name}>
+ {scheduler.name}
+ </option>
+ ))}
+ </Input>
+ </FormGroup>
+ </Form>
+ </Modal>
+ )
+}
+
+NewScenarioModalComponent.propTypes = {
+ show: PropTypes.bool.isRequired,
+ currentPortfolioId: PropTypes.string.isRequired,
+ currentPortfolioScenarioIds: PropTypes.arrayOf(PropTypes.string),
+ traces: PropTypes.arrayOf(Shapes.Trace),
+ topologies: PropTypes.arrayOf(Shapes.Topology),
+ schedulers: PropTypes.arrayOf(Shapes.Scheduler),
+ callback: PropTypes.func.isRequired,
+}
+
+export default NewScenarioModalComponent
diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js
new file mode 100644
index 00000000..9fee8831
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js
@@ -0,0 +1,71 @@
+import PropTypes from 'prop-types'
+import { Form, FormGroup, Input, Label } from 'reactstrap'
+import React, { useRef } from 'react'
+import Shapes from '../../../shapes'
+import Modal from '../Modal'
+
+const NewTopologyModalComponent = ({ show, onCreateTopology, onDuplicateTopology, onCancel, topologies }) => {
+ const form = useRef(null)
+ const textInput = useRef(null)
+ const originTopology = useRef(null)
+
+ const onCreate = () => {
+ onCreateTopology(textInput.current.value)
+ }
+
+ const onDuplicate = () => {
+ onDuplicateTopology(textInput.current.value, originTopology.current.value)
+ }
+
+ const onSubmit = () => {
+ if (!form.current.reportValidity()) {
+ return false
+ } else if (originTopology.current.selectedIndex === 0) {
+ onCreate()
+ } else {
+ onDuplicate()
+ }
+
+ return true
+ }
+
+ return (
+ <Modal title="New Topology" show={show} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form
+ onSubmit={(e) => {
+ e.preventDefault()
+ onSubmit()
+ }}
+ innerRef={form}
+ >
+ <FormGroup>
+ <Label for="name">Name</Label>
+ <Input name="name" type="text" required innerRef={textInput} />
+ </FormGroup>
+ <FormGroup>
+ <Label for="origin">Topology to duplicate</Label>
+ <Input name="origin" type="select" innerRef={originTopology}>
+ <option value={-1} key={-1}>
+ None - start from scratch
+ </option>
+ {topologies.map((topology) => (
+ <option value={topology._id} key={topology._id}>
+ {topology.name}
+ </option>
+ ))}
+ </Input>
+ </FormGroup>
+ </Form>
+ </Modal>
+ )
+}
+
+NewTopologyModalComponent.propTypes = {
+ show: PropTypes.bool.isRequired,
+ topologies: PropTypes.arrayOf(Shapes.Topology),
+ onCreateTopology: PropTypes.func.isRequired,
+ onDuplicateTopology: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+}
+
+export default NewTopologyModalComponent
diff --git a/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js b/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js
new file mode 100644
index 00000000..c5de3d0b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import FontAwesome from 'react-fontawesome'
+import { Link } from 'react-router-dom'
+import { NavLink } from 'reactstrap'
+import Navbar, { NavItem } from './Navbar'
+import './Navbar.sass'
+
+const AppNavbarComponent = ({ project, fullWidth }) => (
+ <Navbar fullWidth={fullWidth}>
+ <NavItem route="/projects">
+ <NavLink tag={Link} title="My Projects" to="/projects">
+ <FontAwesome name="list" className="mr-2" />
+ My Projects
+ </NavLink>
+ </NavItem>
+ {project ? (
+ <NavItem>
+ <NavLink tag={Link} title="Current Project" to={`/projects/${project._id}`}>
+ <span>{project.name}</span>
+ </NavLink>
+ </NavItem>
+ ) : undefined}
+ </Navbar>
+)
+
+export default AppNavbarComponent
diff --git a/opendc-web/opendc-web-ui/src/components/navigation/HomeNavbar.js b/opendc-web/opendc-web-ui/src/components/navigation/HomeNavbar.js
new file mode 100644
index 00000000..08d222ea
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/navigation/HomeNavbar.js
@@ -0,0 +1,23 @@
+import React from 'react'
+import { NavItem, NavLink } from 'reactstrap'
+import Navbar from './Navbar'
+import './Navbar.sass'
+
+const ScrollNavItem = ({ id, name }) => (
+ <NavItem>
+ <NavLink href={id}>{name}</NavLink>
+ </NavItem>
+)
+
+const HomeNavbar = () => (
+ <Navbar fullWidth={false}>
+ <ScrollNavItem id="#stakeholders" name="Stakeholders" />
+ <ScrollNavItem id="#modeling" name="Modeling" />
+ <ScrollNavItem id="#project" name="Project" />
+ <ScrollNavItem id="#technologies" name="Technologies" />
+ <ScrollNavItem id="#team" name="Team" />
+ <ScrollNavItem id="#contact" name="Contact" />
+ </Navbar>
+)
+
+export default HomeNavbar
diff --git a/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js b/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js
new file mode 100644
index 00000000..78b02b44
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js
@@ -0,0 +1,17 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import FontAwesome from 'react-fontawesome'
+import { Link } from 'react-router-dom'
+import { NavLink } from 'reactstrap'
+
+const LogoutButton = ({ onLogout }) => (
+ <NavLink tag={Link} className="logout" title="Sign out" to="#" onClick={onLogout}>
+ <FontAwesome name="power-off" size="lg" />
+ </NavLink>
+)
+
+LogoutButton.propTypes = {
+ onLogout: PropTypes.func.isRequired,
+}
+
+export default LogoutButton
diff --git a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js
new file mode 100644
index 00000000..55f98900
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js
@@ -0,0 +1,92 @@
+import React, { useState } from 'react'
+import { Link, useLocation } from 'react-router-dom'
+import {
+ Navbar as RNavbar,
+ NavItem as RNavItem,
+ NavLink,
+ NavbarBrand,
+ NavbarToggler,
+ Collapse,
+ Nav,
+ Container,
+} from 'reactstrap'
+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.sass'
+
+export const NAVBAR_HEIGHT = 60
+
+const GitHubLink = () => (
+ <a
+ href="https://github.com/atlarge-research/opendc"
+ className="ml-2 mr-3 text-dark"
+ style={{ position: 'relative', top: 7 }}
+ >
+ <span className="fa fa-github fa-2x" />
+ </a>
+)
+
+export const NavItem = ({ route, children }) => {
+ const location = useLocation()
+ return <RNavItem active={location.pathname === route}>{children}</RNavItem>
+}
+
+export const LoggedInSection = () => {
+ const location = useLocation()
+ return (
+ <Nav navbar className="auth-links">
+ {userIsLoggedIn() ? (
+ [
+ location.pathname === '/' ? (
+ <NavItem route="/projects" key="projects">
+ <NavLink tag={Link} title="My Projects" to="/projects">
+ My Projects
+ </NavLink>
+ </NavItem>
+ ) : (
+ <NavItem route="/profile" key="profile">
+ <NavLink tag={Link} title="My Profile" to="/profile">
+ <ProfileName />
+ </NavLink>
+ </NavItem>
+ ),
+ <NavItem route="logout" key="logout">
+ <Logout />
+ </NavItem>,
+ ]
+ ) : (
+ <NavItem route="login">
+ <GitHubLink />
+ <Login visible={true} />
+ </NavItem>
+ )}
+ </Nav>
+ )
+}
+
+const Navbar = ({ fullWidth, children }) => {
+ const [isOpen, setIsOpen] = useState(false)
+ const toggle = () => setIsOpen(!isOpen)
+
+ return (
+ <RNavbar fixed="top" color="light" light expand="lg" id="navbar">
+ <Container fluid={fullWidth}>
+ <NavbarToggler onClick={toggle} />
+ <NavbarBrand tag={Link} to="/" title="OpenDC" className="opendc-brand">
+ <img src="/img/logo.png" alt="OpenDC" />
+ </NavbarBrand>
+
+ <Collapse isOpen={isOpen} navbar>
+ <Nav className="mr-auto" navbar>
+ {children}
+ </Nav>
+ <LoggedInSection />
+ </Collapse>
+ </Container>
+ </RNavbar>
+ )
+}
+
+export default Navbar
diff --git a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.sass b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.sass
new file mode 100644
index 00000000..c9d2aad2
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.sass
@@ -0,0 +1,30 @@
+@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
+ padding-top: 10px
+ +clickable
+
+ &:hover
+ background: $blue-dark
diff --git a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js b/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js
new file mode 100644
index 00000000..dbdba212
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js
@@ -0,0 +1,6 @@
+import React from 'react'
+import './BlinkingCursor.sass'
+
+const BlinkingCursor = () => <span className="blinking-cursor">_</span>
+
+export default BlinkingCursor
diff --git a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.sass b/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.sass
new file mode 100644
index 00000000..ad91df85
--- /dev/null
+++ b/opendc-web/opendc-web-ui/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/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js
new file mode 100644
index 00000000..bcc522c9
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import './CodeBlock.sass'
+
+const CodeBlock = () => {
+ const textBlock =
+ ' oo oooo oo <br/>' +
+ ' oo oo oo oo <br/>' +
+ ' oo oo oo oo <br/>' +
+ ' oooooo oo oo oooooo <br/>' +
+ ' oo oo oo oo <br/>' +
+ ' oo oooo oo <br/>'
+ 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 <div className="code-block" dangerouslySetInnerHTML={{ __html: textBlock }} />
+}
+
+export default CodeBlock
diff --git a/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.sass b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.sass
new file mode 100644
index 00000000..e452f917
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.sass
@@ -0,0 +1,3 @@
+.code-block
+ white-space: pre-wrap
+ margin-top: 60px
diff --git a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js
new file mode 100644
index 00000000..a25e558a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+import BlinkingCursor from './BlinkingCursor'
+import CodeBlock from './CodeBlock'
+import './TerminalWindow.sass'
+
+const TerminalWindow = () => (
+ <div className="terminal-window">
+ <div className="terminal-header">Terminal -- bash</div>
+ <div className="terminal-body">
+ <div className="segfault">
+ $ status
+ <br />
+ opendc[4264]: segfault at 0000051497be459d1 err 12 in libopendc.9.0.4
+ <br />
+ opendc[4269]: segfault at 000004234855fc2db err 3 in libopendc.9.0.4
+ <br />
+ opendc[4270]: STDERR Page does not exist
+ <br />
+ </div>
+ <CodeBlock />
+ <div className="sub-title">
+ Got lost?
+ <BlinkingCursor />
+ </div>
+ <Link to="/" className="home-btn">
+ <span className="fa fa-home" /> GET ME BACK TO OPENDC
+ </Link>
+ </div>
+ </div>
+)
+
+export default TerminalWindow
diff --git a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.sass b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.sass
new file mode 100644
index 00000000..7f05335a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/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/opendc-web/opendc-web-ui/src/components/projects/FilterButton.js b/opendc-web/opendc-web-ui/src/components/projects/FilterButton.js
new file mode 100644
index 00000000..664f9b46
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/FilterButton.js
@@ -0,0 +1,24 @@
+import classNames from 'classnames'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+const FilterButton = ({ active, children, onClick }) => (
+ <button
+ className={classNames('btn btn-secondary', { active: active })}
+ onClick={() => {
+ if (!active) {
+ onClick()
+ }
+ }}
+ >
+ {children}
+ </button>
+)
+
+FilterButton.propTypes = {
+ active: PropTypes.bool.isRequired,
+ children: PropTypes.node.isRequired,
+ onClick: PropTypes.func.isRequired,
+}
+
+export default FilterButton
diff --git a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js
new file mode 100644
index 00000000..2b9795d0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import FilterLink from '../../containers/projects/FilterLink'
+import './FilterPanel.sass'
+
+const FilterPanel = () => (
+ <div className="btn-group filter-panel mb-2">
+ <FilterLink filter="SHOW_ALL">All Projects</FilterLink>
+ <FilterLink filter="SHOW_OWN">My Projects</FilterLink>
+ <FilterLink filter="SHOW_SHARED">Shared with me</FilterLink>
+ </div>
+)
+
+export default FilterPanel
diff --git a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.sass b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.sass
new file mode 100644
index 00000000..f71cf6c8
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.sass
@@ -0,0 +1,5 @@
+.filter-panel
+ display: flex
+
+ button
+ flex: 1 !important
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewProjectButtonComponent.js b/opendc-web/opendc-web-ui/src/components/projects/NewProjectButtonComponent.js
new file mode 100644
index 00000000..312671c6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewProjectButtonComponent.js
@@ -0,0 +1,17 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+
+const NewProjectButtonComponent = ({ onClick }) => (
+ <div className="bottom-btn-container">
+ <div className="btn btn-primary float-right" onClick={onClick}>
+ <span className="fa fa-plus mr-2" />
+ New Project
+ </div>
+ </div>
+)
+
+NewProjectButtonComponent.propTypes = {
+ onClick: PropTypes.func.isRequired,
+}
+
+export default NewProjectButtonComponent
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js
new file mode 100644
index 00000000..1c76cc7f
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js
@@ -0,0 +1,29 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+const ProjectActionButtons = ({ projectId, onViewUsers, onDelete }) => (
+ <td className="text-right">
+ <Link to={'/projects/' + projectId} className="btn btn-outline-primary btn-sm mr-2" title="Open this project">
+ <span className="fa fa-play" />
+ </Link>
+ <div
+ className="btn btn-outline-success btn-sm disabled mr-2"
+ title="View and edit collaborators (not supported currently)"
+ onClick={() => onViewUsers(projectId)}
+ >
+ <span className="fa fa-users" />
+ </div>
+ <div className="btn btn-outline-danger btn-sm" title="Delete this project" onClick={() => onDelete(projectId)}>
+ <span className="fa fa-trash" />
+ </div>
+ </td>
+)
+
+ProjectActionButtons.propTypes = {
+ projectId: PropTypes.string.isRequired,
+ onViewUsers: PropTypes.func,
+ onDelete: PropTypes.func,
+}
+
+export default ProjectActionButtons
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthList.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthList.js
new file mode 100644
index 00000000..8eb4f93b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthList.js
@@ -0,0 +1,39 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import Shapes from '../../shapes/index'
+import ProjectAuthRow from './ProjectAuthRow'
+
+const ProjectAuthList = ({ authorizations }) => {
+ return (
+ <div className="vertically-expanding-container">
+ {authorizations.length === 0 ? (
+ <div className="alert alert-info">
+ <span className="info-icon fa fa-question-circle mr-2" />
+ <strong>No projects here yet...</strong> Add some with the 'New Project' button!
+ </div>
+ ) : (
+ <table className="table table-striped">
+ <thead>
+ <tr>
+ <th>Project name</th>
+ <th>Last edited</th>
+ <th>Access rights</th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {authorizations.map((authorization) => (
+ <ProjectAuthRow projectAuth={authorization} key={authorization.project._id} />
+ ))}
+ </tbody>
+ </table>
+ )}
+ </div>
+ )
+}
+
+ProjectAuthList.propTypes = {
+ authorizations: PropTypes.arrayOf(Shapes.Authorization).isRequired,
+}
+
+export default ProjectAuthList
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthRow.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthRow.js
new file mode 100644
index 00000000..3f904061
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthRow.js
@@ -0,0 +1,24 @@
+import classNames from 'classnames'
+import React from 'react'
+import ProjectActions from '../../containers/projects/ProjectActions'
+import Shapes from '../../shapes/index'
+import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from '../../util/authorizations'
+import { parseAndFormatDateTime } from '../../util/date-time'
+
+const ProjectAuthRow = ({ projectAuth }) => (
+ <tr>
+ <td className="pt-3">{projectAuth.project.name}</td>
+ <td className="pt-3">{parseAndFormatDateTime(projectAuth.project.datetimeLastEdited)}</td>
+ <td className="pt-3">
+ <span className={classNames('fa', 'fa-' + AUTH_ICON_MAP[projectAuth.authorizationLevel], 'mr-2')} />
+ {AUTH_DESCRIPTION_MAP[projectAuth.authorizationLevel]}
+ </td>
+ <ProjectActions projectId={projectAuth.project._id} />
+ </tr>
+)
+
+ProjectAuthRow.propTypes = {
+ projectAuth: Shapes.Authorization.isRequired,
+}
+
+export default ProjectAuthRow
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js
new file mode 100644
index 00000000..9e4a6969
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { goDownOneInteractionLevel } from '../../../actions/interaction-level'
+import GrayLayer from '../../../components/app/map/elements/GrayLayer'
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: () => dispatch(goDownOneInteractionLevel()),
+ }
+}
+
+const GrayContainer = connect(undefined, mapDispatchToProps)(GrayLayer)
+
+export default GrayContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js b/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js
new file mode 100644
index 00000000..23c920b6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux'
+import { setMapDimensions, setMapPositionWithBoundsCheck, zoomInOnPosition } from '../../../actions/map'
+import MapStageComponent from '../../../components/app/map/MapStageComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ mapPosition: state.map.position,
+ mapDimensions: state.map.dimensions,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ zoomInOnPosition: (zoomIn, x, y) => dispatch(zoomInOnPosition(zoomIn, x, y)),
+ setMapPositionWithBoundsCheck: (x, y) => dispatch(setMapPositionWithBoundsCheck(x, y)),
+ setMapDimensions: (width, height) => dispatch(setMapDimensions(width, height)),
+ }
+}
+
+const MapStage = connect(mapStateToProps, mapDispatchToProps)(MapStageComponent)
+
+export default MapStage
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js
new file mode 100644
index 00000000..40077608
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import RackGroup from '../../../components/app/map/groups/RackGroup'
+
+const mapStateToProps = (state) => {
+ return {
+ interactionLevel: state.interactionLevel,
+ }
+}
+
+const RackContainer = connect(mapStateToProps)(RackGroup)
+
+export default RackContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js
new file mode 100644
index 00000000..53746271
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux'
+import RackFillBar from '../../../components/app/map/elements/RackFillBar'
+
+const mapStateToProps = (state, ownProps) => {
+ let energyConsumptionTotal = 0
+ const rack = state.objects.rack[state.objects.tile[ownProps.tileId].rackId]
+ const machineIds = rack.machineIds
+ machineIds.forEach((machineId) => {
+ if (machineId !== null) {
+ const machine = state.objects.machine[machineId]
+ machine.cpuIds.forEach((id) => (energyConsumptionTotal += state.objects.cpu[id].energyConsumptionW))
+ machine.gpuIds.forEach((id) => (energyConsumptionTotal += state.objects.gpu[id].energyConsumptionW))
+ machine.memoryIds.forEach((id) => (energyConsumptionTotal += state.objects.memory[id].energyConsumptionW))
+ machine.storageIds.forEach((id) => (energyConsumptionTotal += state.objects.storage[id].energyConsumptionW))
+ }
+ })
+
+ return {
+ type: 'energy',
+ fillFraction: Math.min(1, energyConsumptionTotal / rack.powerCapacityW),
+ }
+}
+
+const RackSpaceFillContainer = connect(mapStateToProps)(RackFillBar)
+
+export default RackSpaceFillContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js
new file mode 100644
index 00000000..0509a5a5
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js
@@ -0,0 +1,14 @@
+import { connect } from 'react-redux'
+import RackFillBar from '../../../components/app/map/elements/RackFillBar'
+
+const mapStateToProps = (state, ownProps) => {
+ const machineIds = state.objects.rack[state.objects.tile[ownProps.tileId].rackId].machineIds
+ return {
+ type: 'space',
+ fillFraction: machineIds.filter((id) => id !== null).length / machineIds.length,
+ }
+}
+
+const RackSpaceFillContainer = connect(mapStateToProps)(RackFillBar)
+
+export default RackSpaceFillContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js
new file mode 100644
index 00000000..91bf4e5d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import { goFromBuildingToRoom } from '../../../actions/interaction-level'
+import RoomGroup from '../../../components/app/map/groups/RoomGroup'
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ interactionLevel: state.interactionLevel,
+ currentRoomInConstruction: state.construction.currentRoomInConstruction,
+ room: state.objects.room[ownProps.roomId],
+ }
+}
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ onClick: () => dispatch(goFromBuildingToRoom(ownProps.roomId)),
+ }
+}
+
+const RoomContainer = connect(mapStateToProps, mapDispatchToProps)(RoomGroup)
+
+export default RoomContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js
new file mode 100644
index 00000000..04d6c8d6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux'
+import { goFromRoomToRack } from '../../../actions/interaction-level'
+import TileGroup from '../../../components/app/map/groups/TileGroup'
+
+const mapStateToProps = (state, ownProps) => {
+ const tile = state.objects.tile[ownProps.tileId]
+
+ return {
+ interactionLevel: state.interactionLevel,
+ tile,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: (tile) => {
+ if (tile.rackId) {
+ dispatch(goFromRoomToRack(tile._id))
+ }
+ },
+ }
+}
+
+const TileContainer = connect(mapStateToProps, mapDispatchToProps)(TileGroup)
+
+export default TileContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js
new file mode 100644
index 00000000..de43a151
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux'
+import TopologyGroup from '../../../components/app/map/groups/TopologyGroup'
+
+const mapStateToProps = (state) => {
+ if (state.currentTopologyId === '-1') {
+ return {}
+ }
+
+ return {
+ topology: state.objects.topology[state.currentTopologyId],
+ interactionLevel: state.interactionLevel,
+ }
+}
+
+const TopologyContainer = connect(mapStateToProps)(TopologyGroup)
+
+export default TopologyContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js
new file mode 100644
index 00000000..67f8a242
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import WallGroup from '../../../components/app/map/groups/WallGroup'
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ tiles: state.objects.room[ownProps.roomId].tileIds.map((tileId) => state.objects.tile[tileId]),
+ }
+}
+
+const WallContainer = connect(mapStateToProps)(WallGroup)
+
+export default WallContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js
new file mode 100644
index 00000000..fa3b9d22
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import ScaleIndicatorComponent from '../../../../components/app/map/controls/ScaleIndicatorComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ scale: state.map.scale,
+ }
+}
+
+const ScaleIndicatorContainer = connect(mapStateToProps)(ScaleIndicatorComponent)
+
+export default ScaleIndicatorContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js
new file mode 100644
index 00000000..ddc68cc7
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux'
+import { zoomInOnCenter } from '../../../../actions/map'
+import ZoomControlComponent from '../../../../components/app/map/controls/ZoomControlComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ mapScale: state.map.scale,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ zoomInOnCenter: (zoomIn) => dispatch(zoomInOnCenter(zoomIn)),
+ }
+}
+
+const ZoomControlContainer = connect(mapStateToProps, mapDispatchToProps)(ZoomControlComponent)
+
+export default ZoomControlContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js b/opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js
new file mode 100644
index 00000000..8596cb9c
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import MapLayerComponent from '../../../../components/app/map/layers/MapLayerComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ mapPosition: state.map.position,
+ mapScale: state.map.scale,
+ }
+}
+
+const MapLayer = connect(mapStateToProps)(MapLayerComponent)
+
+export default MapLayer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js b/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js
new file mode 100644
index 00000000..a4927862
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js
@@ -0,0 +1,33 @@
+import { connect } from 'react-redux'
+import { addRackToTile } from '../../../../actions/topology/room'
+import ObjectHoverLayerComponent from '../../../../components/app/map/layers/ObjectHoverLayerComponent'
+import { findTileWithPosition } from '../../../../util/tile-calculations'
+
+const mapStateToProps = (state) => {
+ return {
+ mapPosition: state.map.position,
+ mapScale: state.map.scale,
+ isEnabled: () => state.construction.inRackConstructionMode,
+ isValid: (x, y) => {
+ if (state.interactionLevel.mode !== 'ROOM') {
+ return false
+ }
+
+ const currentRoom = state.objects.room[state.interactionLevel.roomId]
+ const tiles = currentRoom.tileIds.map((tileId) => state.objects.tile[tileId])
+ const tile = findTileWithPosition(tiles, x, y)
+
+ return !(tile === null || tile.rackId)
+ },
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: (x, y) => dispatch(addRackToTile(x, y)),
+ }
+}
+
+const ObjectHoverLayer = connect(mapStateToProps, mapDispatchToProps)(ObjectHoverLayerComponent)
+
+export default ObjectHoverLayer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js
new file mode 100644
index 00000000..66404f9e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js
@@ -0,0 +1,46 @@
+import { connect } from 'react-redux'
+import { toggleTileAtLocation } from '../../../../actions/topology/building'
+import RoomHoverLayerComponent from '../../../../components/app/map/layers/RoomHoverLayerComponent'
+import {
+ deriveValidNextTilePositions,
+ findPositionInPositions,
+ findPositionInRooms,
+} from '../../../../util/tile-calculations'
+
+const mapStateToProps = (state) => {
+ return {
+ mapPosition: state.map.position,
+ mapScale: state.map.scale,
+ isEnabled: () => state.construction.currentRoomInConstruction !== '-1',
+ isValid: (x, y) => {
+ const newRoom = Object.assign({}, state.objects.room[state.construction.currentRoomInConstruction])
+ const oldRooms = Object.keys(state.objects.room)
+ .map((id) => Object.assign({}, state.objects.room[id]))
+ .filter(
+ (room) =>
+ state.objects.topology[state.currentTopologyId].roomIds.indexOf(room._id) !== -1 &&
+ room._id !== state.construction.currentRoomInConstruction
+ )
+
+ ;[...oldRooms, newRoom].forEach((room) => {
+ room.tiles = room.tileIds.map((tileId) => state.objects.tile[tileId])
+ })
+ if (newRoom.tileIds.length === 0) {
+ return findPositionInRooms(oldRooms, x, y) === -1
+ }
+
+ const validNextPositions = deriveValidNextTilePositions(oldRooms, newRoom.tiles)
+ return findPositionInPositions(validNextPositions, x, y) !== -1
+ },
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: (x, y) => dispatch(toggleTileAtLocation(x, y)),
+ }
+}
+
+const RoomHoverLayer = connect(mapStateToProps, mapDispatchToProps)(RoomHoverLayerComponent)
+
+export default RoomHoverLayer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js b/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js
new file mode 100644
index 00000000..4b430e54
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux'
+import PortfolioResultsComponent from '../../../components/app/results/PortfolioResultsComponent'
+
+const mapStateToProps = (state) => {
+ if (
+ state.currentPortfolioId === '-1' ||
+ !state.objects.portfolio[state.currentPortfolioId] ||
+ state.objects.portfolio[state.currentPortfolioId].scenarioIds
+ .map((scenarioId) => state.objects.scenario[scenarioId])
+ .some((s) => s === undefined)
+ ) {
+ return {
+ portfolio: undefined,
+ scenarios: [],
+ }
+ }
+
+ return {
+ portfolio: state.objects.portfolio[state.currentPortfolioId],
+ scenarios: state.objects.portfolio[state.currentPortfolioId].scenarioIds.map(
+ (scenarioId) => state.objects.scenario[scenarioId]
+ ),
+ }
+}
+
+const PortfolioResultsContainer = connect(mapStateToProps)(PortfolioResultsComponent)
+
+export default PortfolioResultsContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js
new file mode 100644
index 00000000..b32c8b1d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js
@@ -0,0 +1,45 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import PortfolioListComponent from '../../../../components/app/sidebars/project/PortfolioListComponent'
+import { deletePortfolio, setCurrentPortfolio } from '../../../../actions/portfolios'
+import { openNewPortfolioModal } from '../../../../actions/modals/portfolios'
+import { getState } from '../../../../util/state-utils'
+import { setCurrentTopology } from '../../../../actions/topology/building'
+
+const mapStateToProps = (state) => {
+ let portfolios = state.objects.project[state.currentProjectId]
+ ? state.objects.project[state.currentProjectId].portfolioIds.map((t) => state.objects.portfolio[t])
+ : []
+ if (portfolios.filter((t) => !t).length > 0) {
+ portfolios = []
+ }
+
+ return {
+ currentProjectId: state.currentProjectId,
+ currentPortfolioId: state.currentPortfolioId,
+ portfolios,
+ }
+}
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ onNewPortfolio: () => {
+ dispatch(openNewPortfolioModal())
+ },
+ onChoosePortfolio: (portfolioId) => {
+ dispatch(setCurrentPortfolio(portfolioId))
+ },
+ onDeletePortfolio: async (id) => {
+ if (id) {
+ const state = await getState(dispatch)
+ dispatch(deletePortfolio(id))
+ dispatch(setCurrentTopology(state.objects.project[state.currentProjectId].topologyIds[0]))
+ ownProps.history.push(`/projects/${state.currentProjectId}`)
+ }
+ },
+ }
+}
+
+const PortfolioListContainer = withRouter(connect(mapStateToProps, mapDispatchToProps)(PortfolioListComponent))
+
+export default PortfolioListContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js
new file mode 100644
index 00000000..49001099
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js
@@ -0,0 +1,10 @@
+import React from 'react'
+import { withRouter } from 'react-router-dom'
+import ProjectSidebarComponent from '../../../../components/app/sidebars/project/ProjectSidebarComponent'
+import { isCollapsible } from '../../../../util/sidebar-space'
+
+const ProjectSidebarContainer = withRouter(({ location, ...props }) => (
+ <ProjectSidebarComponent collapsible={isCollapsible(location)} {...props} />
+))
+
+export default ProjectSidebarContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js
new file mode 100644
index 00000000..415e2792
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js
@@ -0,0 +1,41 @@
+import { connect } from 'react-redux'
+import ScenarioListComponent from '../../../../components/app/sidebars/project/ScenarioListComponent'
+import { openNewScenarioModal } from '../../../../actions/modals/scenarios'
+import { deleteScenario, setCurrentScenario } from '../../../../actions/scenarios'
+import { setCurrentPortfolio } from '../../../../actions/portfolios'
+
+const mapStateToProps = (state, ownProps) => {
+ let scenarios = state.objects.portfolio[ownProps.portfolioId]
+ ? state.objects.portfolio[ownProps.portfolioId].scenarioIds.map((t) => state.objects.scenario[t])
+ : []
+ if (scenarios.filter((t) => !t).length > 0) {
+ scenarios = []
+ }
+
+ return {
+ currentProjectId: state.currentProjectId,
+ currentScenarioId: state.currentScenarioId,
+ scenarios,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onNewScenario: (currentPortfolioId) => {
+ dispatch(setCurrentPortfolio(currentPortfolioId))
+ dispatch(openNewScenarioModal())
+ },
+ onChooseScenario: (portfolioId, scenarioId) => {
+ dispatch(setCurrentScenario(portfolioId, scenarioId))
+ },
+ onDeleteScenario: (id) => {
+ if (id) {
+ dispatch(deleteScenario(id))
+ }
+ },
+ }
+}
+
+const ScenarioListContainer = connect(mapStateToProps, mapDispatchToProps)(ScenarioListComponent)
+
+export default ScenarioListContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js
new file mode 100644
index 00000000..e1de18f9
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js
@@ -0,0 +1,46 @@
+import { connect } from 'react-redux'
+import TopologyListComponent from '../../../../components/app/sidebars/project/TopologyListComponent'
+import { setCurrentTopology } from '../../../../actions/topology/building'
+import { openNewTopologyModal } from '../../../../actions/modals/topology'
+import { withRouter } from 'react-router-dom'
+import { getState } from '../../../../util/state-utils'
+import { deleteTopology } from '../../../../actions/topologies'
+
+const mapStateToProps = (state) => {
+ let topologies = state.objects.project[state.currentProjectId]
+ ? state.objects.project[state.currentProjectId].topologyIds.map((t) => state.objects.topology[t])
+ : []
+ if (topologies.filter((t) => !t).length > 0) {
+ topologies = []
+ }
+
+ return {
+ currentTopologyId: state.currentTopologyId,
+ topologies,
+ }
+}
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ onChooseTopology: async (id) => {
+ dispatch(setCurrentTopology(id))
+ const state = await getState(dispatch)
+ ownProps.history.push(`/projects/${state.currentProjectId}`)
+ },
+ onNewTopology: () => {
+ dispatch(openNewTopologyModal())
+ },
+ onDeleteTopology: async (id) => {
+ if (id) {
+ const state = await getState(dispatch)
+ dispatch(deleteTopology(id))
+ dispatch(setCurrentTopology(state.objects.project[state.currentProjectId].topologyIds[0]))
+ ownProps.history.push(`/projects/${state.currentProjectId}`)
+ }
+ },
+ }
+}
+
+const TopologyListContainer = withRouter(connect(mapStateToProps, mapDispatchToProps)(TopologyListComponent))
+
+export default TopologyListContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js
new file mode 100644
index 00000000..fe7c02fd
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import TopologySidebarComponent from '../../../../components/app/sidebars/topology/TopologySidebarComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ interactionLevel: state.interactionLevel,
+ }
+}
+
+const TopologySidebarContainer = connect(mapStateToProps)(TopologySidebarComponent)
+
+export default TopologySidebarContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js
new file mode 100644
index 00000000..a0b52e56
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js
@@ -0,0 +1,5 @@
+import BuildingSidebarComponent from '../../../../../components/app/sidebars/topology/building/BuildingSidebarComponent'
+
+const BuildingSidebarContainer = BuildingSidebarComponent
+
+export default BuildingSidebarContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js
new file mode 100644
index 00000000..ea9e9e60
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux'
+import {
+ cancelNewRoomConstruction,
+ finishNewRoomConstruction,
+ startNewRoomConstruction,
+} from '../../../../../actions/topology/building'
+import StartNewRoomConstructionComponent from '../../../../../components/app/sidebars/topology/building/NewRoomConstructionComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ currentRoomInConstruction: state.construction.currentRoomInConstruction,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onStart: () => dispatch(startNewRoomConstruction()),
+ onFinish: () => dispatch(finishNewRoomConstruction()),
+ onCancel: () => dispatch(cancelNewRoomConstruction()),
+ }
+}
+
+const NewRoomConstructionButton = connect(mapStateToProps, mapDispatchToProps)(StartNewRoomConstructionComponent)
+
+export default NewRoomConstructionButton
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js
new file mode 100644
index 00000000..24287ab0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { goDownOneInteractionLevel } from '../../../../../actions/interaction-level'
+import BackToRackComponent from '../../../../../components/app/sidebars/topology/machine/BackToRackComponent'
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: () => dispatch(goDownOneInteractionLevel()),
+ }
+}
+
+const BackToRackContainer = connect(undefined, mapDispatchToProps)(BackToRackComponent)
+
+export default BackToRackContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js
new file mode 100644
index 00000000..65e683e6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { openDeleteMachineModal } from '../../../../../actions/modals/topology'
+import DeleteMachineComponent from '../../../../../components/app/sidebars/topology/machine/DeleteMachineComponent'
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: () => dispatch(openDeleteMachineModal()),
+ }
+}
+
+const DeleteMachineContainer = connect(undefined, mapDispatchToProps)(DeleteMachineComponent)
+
+export default DeleteMachineContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js
new file mode 100644
index 00000000..1cf35b05
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import MachineNameComponent from '../../../../../components/app/sidebars/topology/machine/MachineNameComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ position: state.interactionLevel.position,
+ }
+}
+
+const MachineNameContainer = connect(mapStateToProps)(MachineNameComponent)
+
+export default MachineNameContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js
new file mode 100644
index 00000000..b04e3118
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux'
+import MachineSidebarComponent from '../../../../../components/app/sidebars/topology/machine/MachineSidebarComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ machineId:
+ state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].machineIds[
+ state.interactionLevel.position - 1
+ ],
+ }
+}
+
+const MachineSidebarContainer = connect(mapStateToProps)(MachineSidebarComponent)
+
+export default MachineSidebarContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js
new file mode 100644
index 00000000..29e48016
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux'
+import { addUnit } from '../../../../../actions/topology/machine'
+import UnitAddComponent from '../../../../../components/app/sidebars/topology/machine/UnitAddComponent'
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ units: Object.values(state.objects[ownProps.unitType]),
+ }
+}
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ onAdd: (id) => dispatch(addUnit(ownProps.unitType, id)),
+ }
+}
+
+const UnitAddContainer = connect(mapStateToProps, mapDispatchToProps)(UnitAddComponent)
+
+export default UnitAddContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js
new file mode 100644
index 00000000..f334f9f2
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import { deleteUnit } from '../../../../../actions/topology/machine'
+import UnitComponent from '../../../../../components/app/sidebars/topology/machine/UnitComponent'
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ unit: state.objects[ownProps.unitType][ownProps.unitId],
+ index: ownProps.unitId,
+ }
+}
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ onDelete: () => dispatch(deleteUnit(ownProps.unitType, ownProps.index)),
+ }
+}
+
+const UnitContainer = connect(mapStateToProps, mapDispatchToProps)(UnitComponent)
+
+export default UnitContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js
new file mode 100644
index 00000000..f382ff74
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux'
+import UnitListComponent from '../../../../../components/app/sidebars/topology/machine/UnitListComponent'
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ unitIds:
+ state.objects.machine[
+ state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].machineIds[
+ state.interactionLevel.position - 1
+ ]
+ ][ownProps.unitType + 'Ids'],
+ }
+}
+
+const UnitListContainer = connect(mapStateToProps)(UnitListComponent)
+
+export default UnitListContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js
new file mode 100644
index 00000000..00fe4067
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js
@@ -0,0 +1,5 @@
+import UnitTabsComponent from '../../../../../components/app/sidebars/topology/machine/UnitTabsComponent'
+
+const UnitTabsContainer = UnitTabsComponent
+
+export default UnitTabsContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js
new file mode 100644
index 00000000..c941e745
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { addPrefab } from '../../../../../actions/prefabs'
+import AddPrefabComponent from '../../../../../components/app/sidebars/topology/rack/AddPrefabComponent'
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: () => dispatch(addPrefab('name')),
+ }
+}
+
+const AddPrefabContainer = connect(undefined, mapDispatchToProps)(AddPrefabComponent)
+
+export default AddPrefabContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js
new file mode 100644
index 00000000..58c3b082
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { goDownOneInteractionLevel } from '../../../../../actions/interaction-level'
+import BackToRoomComponent from '../../../../../components/app/sidebars/topology/rack/BackToRoomComponent'
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: () => dispatch(goDownOneInteractionLevel()),
+ }
+}
+
+const BackToRoomContainer = connect(undefined, mapDispatchToProps)(BackToRoomComponent)
+
+export default BackToRoomContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js
new file mode 100644
index 00000000..8229a359
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { openDeleteRackModal } from '../../../../../actions/modals/topology'
+import DeleteRackComponent from '../../../../../components/app/sidebars/topology/rack/DeleteRackComponent'
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: () => dispatch(openDeleteRackModal()),
+ }
+}
+
+const DeleteRackContainer = connect(undefined, mapDispatchToProps)(DeleteRackComponent)
+
+export default DeleteRackContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/EmptySlotContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/EmptySlotContainer.js
new file mode 100644
index 00000000..cf341da9
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/EmptySlotContainer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { addMachine } from '../../../../../actions/topology/rack'
+import EmptySlotComponent from '../../../../../components/app/sidebars/topology/rack/EmptySlotComponent'
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ onAdd: () => dispatch(addMachine(ownProps.position)),
+ }
+}
+
+const EmptySlotContainer = connect(undefined, mapDispatchToProps)(EmptySlotComponent)
+
+export default EmptySlotContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js
new file mode 100644
index 00000000..fe12827d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux'
+import { goFromRackToMachine } from '../../../../../actions/interaction-level'
+import MachineComponent from '../../../../../components/app/sidebars/topology/rack/MachineComponent'
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ machine: state.objects.machine[ownProps.machineId],
+ }
+}
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ onClick: () => dispatch(goFromRackToMachine(ownProps.position)),
+ }
+}
+
+const MachineContainer = connect(mapStateToProps, mapDispatchToProps)(MachineComponent)
+
+export default MachineContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js
new file mode 100644
index 00000000..bc5a285a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import MachineListComponent from '../../../../../components/app/sidebars/topology/rack/MachineListComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ machineIds: state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].machineIds,
+ }
+}
+
+const MachineListContainer = connect(mapStateToProps)(MachineListComponent)
+
+export default MachineListContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js
new file mode 100644
index 00000000..504dbc61
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux'
+import { openEditRackNameModal } from '../../../../../actions/modals/topology'
+import RackNameComponent from '../../../../../components/app/sidebars/topology/rack/RackNameComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ rackName: state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].name,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onEdit: () => dispatch(openEditRackNameModal()),
+ }
+}
+
+const RackNameContainer = connect(mapStateToProps, mapDispatchToProps)(RackNameComponent)
+
+export default RackNameContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js
new file mode 100644
index 00000000..453d7e41
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import RackSidebarComponent from '../../../../../components/app/sidebars/topology/rack/RackSidebarComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ rackId: state.objects.tile[state.interactionLevel.tileId].rackId,
+ }
+}
+
+const RackSidebarContainer = connect(mapStateToProps)(RackSidebarComponent)
+
+export default RackSidebarContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js
new file mode 100644
index 00000000..4c1ab99d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { goDownOneInteractionLevel } from '../../../../../actions/interaction-level'
+import BackToBuildingComponent from '../../../../../components/app/sidebars/topology/room/BackToBuildingComponent'
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: () => dispatch(goDownOneInteractionLevel()),
+ }
+}
+
+const BackToBuildingContainer = connect(undefined, mapDispatchToProps)(BackToBuildingComponent)
+
+export default BackToBuildingContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js
new file mode 100644
index 00000000..636fa5c5
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { openDeleteRoomModal } from '../../../../../actions/modals/topology'
+import DeleteRoomComponent from '../../../../../components/app/sidebars/topology/room/DeleteRoomComponent'
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: () => dispatch(openDeleteRoomModal()),
+ }
+}
+
+const DeleteRoomContainer = connect(undefined, mapDispatchToProps)(DeleteRoomComponent)
+
+export default DeleteRoomContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js
new file mode 100644
index 00000000..d17a45d1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import { finishRoomEdit, startRoomEdit } from '../../../../../actions/topology/building'
+import EditRoomComponent from '../../../../../components/app/sidebars/topology/room/EditRoomComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ isEditing: state.construction.currentRoomInConstruction !== '-1',
+ isInRackConstructionMode: state.construction.inRackConstructionMode,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onEdit: () => dispatch(startRoomEdit()),
+ onFinish: () => dispatch(finishRoomEdit()),
+ }
+}
+
+const EditRoomContainer = connect(mapStateToProps, mapDispatchToProps)(EditRoomComponent)
+
+export default EditRoomContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js
new file mode 100644
index 00000000..cd8319de
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import { startRackConstruction, stopRackConstruction } from '../../../../../actions/topology/room'
+import RackConstructionComponent from '../../../../../components/app/sidebars/topology/room/RackConstructionComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ inRackConstructionMode: state.construction.inRackConstructionMode,
+ isEditingRoom: state.construction.currentRoomInConstruction !== '-1',
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onStart: () => dispatch(startRackConstruction()),
+ onStop: () => dispatch(stopRackConstruction()),
+ }
+}
+
+const RackConstructionContainer = connect(mapStateToProps, mapDispatchToProps)(RackConstructionComponent)
+
+export default RackConstructionContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js
new file mode 100644
index 00000000..cab16016
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux'
+import { openEditRoomNameModal } from '../../../../../actions/modals/topology'
+import RoomNameComponent from '../../../../../components/app/sidebars/topology/room/RoomNameComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ roomName: state.objects.room[state.interactionLevel.roomId].name,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onEdit: () => dispatch(openEditRoomNameModal()),
+ }
+}
+
+const RoomNameContainer = connect(mapStateToProps, mapDispatchToProps)(RoomNameComponent)
+
+export default RoomNameContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js
new file mode 100644
index 00000000..8c3ca8ab
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import RoomSidebarComponent from '../../../../../components/app/sidebars/topology/room/RoomSidebarComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ roomId: state.interactionLevel.roomId,
+ }
+}
+
+const RoomSidebarContainer = connect(mapStateToProps)(RoomSidebarComponent)
+
+export default RoomSidebarContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/auth/Login.js b/opendc-web/opendc-web-ui/src/containers/auth/Login.js
new file mode 100644
index 00000000..2f9726bf
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/auth/Login.js
@@ -0,0 +1,62 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import GoogleLogin from 'react-google-login'
+import { connect } from 'react-redux'
+import { logIn } from '../../actions/auth'
+
+class LoginContainer extends React.Component {
+ static propTypes = {
+ visible: PropTypes.bool.isRequired,
+ onLogin: PropTypes.func.isRequired,
+ }
+
+ onAuthResponse(response) {
+ this.props.onLogin({
+ email: response.getBasicProfile().getEmail(),
+ givenName: response.getBasicProfile().getGivenName(),
+ familyName: response.getBasicProfile().getFamilyName(),
+ googleId: response.googleId,
+ authToken: response.getAuthResponse().id_token,
+ expiresAt: response.getAuthResponse().expires_at,
+ })
+ }
+
+ onAuthFailure(error) {
+ console.error(error)
+ }
+
+ render() {
+ if (!this.props.visible) {
+ return <span />
+ }
+
+ return (
+ <GoogleLogin
+ clientId={process.env.REACT_APP_OAUTH_CLIENT_ID}
+ onSuccess={this.onAuthResponse.bind(this)}
+ onFailure={this.onAuthFailure.bind(this)}
+ render={(renderProps) => (
+ <span onClick={renderProps.onClick} className="login btn btn-primary">
+ <span className="fa fa-google" /> Login with Google
+ </span>
+ )}
+ ></GoogleLogin>
+ )
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ visible: ownProps.visible,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onLogin: (payload) => dispatch(logIn(payload)),
+ }
+}
+
+const Login = connect(mapStateToProps, mapDispatchToProps)(LoginContainer)
+
+export default Login
diff --git a/opendc-web/opendc-web-ui/src/containers/auth/Logout.js b/opendc-web/opendc-web-ui/src/containers/auth/Logout.js
new file mode 100644
index 00000000..22400381
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/auth/Logout.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { logOut } from '../../actions/auth'
+import LogoutButton from '../../components/navigation/LogoutButton'
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onLogout: () => dispatch(logOut()),
+ }
+}
+
+const Logout = connect(undefined, mapDispatchToProps)(LogoutButton)
+
+export default Logout
diff --git a/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js b/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js
new file mode 100644
index 00000000..06da75ab
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { connect } from 'react-redux'
+
+const mapStateToProps = (state) => {
+ return {
+ text: state.auth.givenName + ' ' + state.auth.familyName,
+ }
+}
+
+const SpanElement = ({ text }) => <span>{text}</span>
+
+const ProfileName = connect(mapStateToProps)(SpanElement)
+
+export default ProfileName
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/DeleteMachineModal.js b/opendc-web/opendc-web-ui/src/containers/modals/DeleteMachineModal.js
new file mode 100644
index 00000000..f30febdb
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/modals/DeleteMachineModal.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { closeDeleteMachineModal } from '../../actions/modals/topology'
+import { deleteMachine } from '../../actions/topology/machine'
+import ConfirmationModal from '../../components/modals/ConfirmationModal'
+
+const DeleteMachineModalComponent = ({ visible, callback }) => (
+ <ConfirmationModal
+ title="Delete this machine"
+ message="Are you sure you want to delete this machine?"
+ show={visible}
+ callback={callback}
+ />
+)
+
+const mapStateToProps = (state) => {
+ return {
+ visible: state.modals.deleteMachineModalVisible,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ callback: (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteMachine())
+ }
+ dispatch(closeDeleteMachineModal())
+ },
+ }
+}
+
+const DeleteMachineModal = connect(mapStateToProps, mapDispatchToProps)(DeleteMachineModalComponent)
+
+export default DeleteMachineModal
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js b/opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js
new file mode 100644
index 00000000..e7c4014d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { closeDeleteProfileModal } from '../../actions/modals/profile'
+import { deleteCurrentUser } from '../../actions/users'
+import ConfirmationModal from '../../components/modals/ConfirmationModal'
+
+const DeleteProfileModalComponent = ({ visible, callback }) => (
+ <ConfirmationModal
+ title="Delete my account"
+ message="Are you sure you want to delete your OpenDC account?"
+ show={visible}
+ callback={callback}
+ />
+)
+
+const mapStateToProps = (state) => {
+ return {
+ visible: state.modals.deleteProfileModalVisible,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ callback: (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteCurrentUser())
+ }
+ dispatch(closeDeleteProfileModal())
+ },
+ }
+}
+
+const DeleteProfileModal = connect(mapStateToProps, mapDispatchToProps)(DeleteProfileModalComponent)
+
+export default DeleteProfileModal
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/DeleteRackModal.js b/opendc-web/opendc-web-ui/src/containers/modals/DeleteRackModal.js
new file mode 100644
index 00000000..0cb22a7e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/modals/DeleteRackModal.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { closeDeleteRackModal } from '../../actions/modals/topology'
+import { deleteRack } from '../../actions/topology/rack'
+import ConfirmationModal from '../../components/modals/ConfirmationModal'
+
+const DeleteRackModalComponent = ({ visible, callback }) => (
+ <ConfirmationModal
+ title="Delete this rack"
+ message="Are you sure you want to delete this rack?"
+ show={visible}
+ callback={callback}
+ />
+)
+
+const mapStateToProps = (state) => {
+ return {
+ visible: state.modals.deleteRackModalVisible,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ callback: (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteRack())
+ }
+ dispatch(closeDeleteRackModal())
+ },
+ }
+}
+
+const DeleteRackModal = connect(mapStateToProps, mapDispatchToProps)(DeleteRackModalComponent)
+
+export default DeleteRackModal
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/DeleteRoomModal.js b/opendc-web/opendc-web-ui/src/containers/modals/DeleteRoomModal.js
new file mode 100644
index 00000000..1f6eef92
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/modals/DeleteRoomModal.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { closeDeleteRoomModal } from '../../actions/modals/topology'
+import { deleteRoom } from '../../actions/topology/room'
+import ConfirmationModal from '../../components/modals/ConfirmationModal'
+
+const DeleteRoomModalComponent = ({ visible, callback }) => (
+ <ConfirmationModal
+ title="Delete this room"
+ message="Are you sure you want to delete this room?"
+ show={visible}
+ callback={callback}
+ />
+)
+
+const mapStateToProps = (state) => {
+ return {
+ visible: state.modals.deleteRoomModalVisible,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ callback: (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteRoom())
+ }
+ dispatch(closeDeleteRoomModal())
+ },
+ }
+}
+
+const DeleteRoomModal = connect(mapStateToProps, mapDispatchToProps)(DeleteRoomModalComponent)
+
+export default DeleteRoomModal
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/EditRackNameModal.js b/opendc-web/opendc-web-ui/src/containers/modals/EditRackNameModal.js
new file mode 100644
index 00000000..9128f449
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/modals/EditRackNameModal.js
@@ -0,0 +1,40 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { closeEditRackNameModal } from '../../actions/modals/topology'
+import { editRackName } from '../../actions/topology/rack'
+import TextInputModal from '../../components/modals/TextInputModal'
+
+const EditRackNameModalComponent = ({ visible, previousName, callback }) => (
+ <TextInputModal
+ title="Edit rack name"
+ label="Rack name"
+ show={visible}
+ initialValue={previousName}
+ callback={callback}
+ />
+)
+
+const mapStateToProps = (state) => {
+ return {
+ visible: state.modals.editRackNameModalVisible,
+ previousName:
+ state.interactionLevel.mode === 'RACK'
+ ? state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].name
+ : '',
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ callback: (name) => {
+ if (name) {
+ dispatch(editRackName(name))
+ }
+ dispatch(closeEditRackNameModal())
+ },
+ }
+}
+
+const EditRackNameModal = connect(mapStateToProps, mapDispatchToProps)(EditRackNameModalComponent)
+
+export default EditRackNameModal
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/EditRoomNameModal.js b/opendc-web/opendc-web-ui/src/containers/modals/EditRoomNameModal.js
new file mode 100644
index 00000000..8032a5d1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/modals/EditRoomNameModal.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { closeEditRoomNameModal } from '../../actions/modals/topology'
+import { editRoomName } from '../../actions/topology/room'
+import TextInputModal from '../../components/modals/TextInputModal'
+
+const EditRoomNameModalComponent = ({ visible, previousName, callback }) => (
+ <TextInputModal
+ title="Edit room name"
+ label="Room name"
+ show={visible}
+ initialValue={previousName}
+ callback={callback}
+ />
+)
+
+const mapStateToProps = (state) => {
+ return {
+ visible: state.modals.editRoomNameModalVisible,
+ previousName:
+ state.interactionLevel.mode === 'ROOM' ? state.objects.room[state.interactionLevel.roomId].name : '',
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ callback: (name) => {
+ if (name) {
+ dispatch(editRoomName(name))
+ }
+ dispatch(closeEditRoomNameModal())
+ },
+ }
+}
+
+const EditRoomNameModal = connect(mapStateToProps, mapDispatchToProps)(EditRoomNameModalComponent)
+
+export default EditRoomNameModal
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/NewPortfolioModal.js b/opendc-web/opendc-web-ui/src/containers/modals/NewPortfolioModal.js
new file mode 100644
index 00000000..6cf12d8e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/modals/NewPortfolioModal.js
@@ -0,0 +1,30 @@
+import { connect } from 'react-redux'
+import NewPortfolioModalComponent from '../../components/modals/custom-components/NewPortfolioModalComponent'
+import { addPortfolio } from '../../actions/portfolios'
+import { closeNewPortfolioModal } from '../../actions/modals/portfolios'
+
+const mapStateToProps = (state) => {
+ return {
+ show: state.modals.newPortfolioModalVisible,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ callback: (name, targets) => {
+ if (name) {
+ dispatch(
+ addPortfolio({
+ name,
+ targets,
+ })
+ )
+ }
+ dispatch(closeNewPortfolioModal())
+ },
+ }
+}
+
+const NewPortfolioModal = connect(mapStateToProps, mapDispatchToProps)(NewPortfolioModalComponent)
+
+export default NewPortfolioModal
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/NewProjectModal.js b/opendc-web/opendc-web-ui/src/containers/modals/NewProjectModal.js
new file mode 100644
index 00000000..d306dc45
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/modals/NewProjectModal.js
@@ -0,0 +1,30 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { closeNewProjectModal } from '../../actions/modals/projects'
+import { addProject } from '../../actions/projects'
+import TextInputModal from '../../components/modals/TextInputModal'
+
+const NewProjectModalComponent = ({ visible, callback }) => (
+ <TextInputModal title="New Project" label="Project title" show={visible} callback={callback} />
+)
+
+const mapStateToProps = (state) => {
+ return {
+ visible: state.modals.newProjectModalVisible,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ callback: (text) => {
+ if (text) {
+ dispatch(addProject(text))
+ }
+ dispatch(closeNewProjectModal())
+ },
+ }
+}
+
+const NewProjectModal = connect(mapStateToProps, mapDispatchToProps)(NewProjectModalComponent)
+
+export default NewProjectModal
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js b/opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js
new file mode 100644
index 00000000..7d774fa4
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js
@@ -0,0 +1,50 @@
+import { connect } from 'react-redux'
+import NewScenarioModalComponent from '../../components/modals/custom-components/NewScenarioModalComponent'
+import { addScenario } from '../../actions/scenarios'
+import { closeNewScenarioModal } from '../../actions/modals/scenarios'
+
+const mapStateToProps = (state) => {
+ let topologies =
+ state.currentProjectId !== '-1'
+ ? state.objects.project[state.currentProjectId].topologyIds.map((t) => state.objects.topology[t])
+ : []
+ if (topologies.filter((t) => !t).length > 0) {
+ topologies = []
+ }
+
+ return {
+ show: state.modals.newScenarioModalVisible,
+ currentPortfolioId: state.currentPortfolioId,
+ currentPortfolioScenarioIds:
+ state.currentPortfolioId !== '-1' && state.objects.portfolio[state.currentPortfolioId]
+ ? state.objects.portfolio[state.currentPortfolioId].scenarioIds
+ : [],
+ traces: Object.values(state.objects.trace),
+ topologies,
+ schedulers: Object.values(state.objects.scheduler),
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ callback: (name, portfolioId, trace, topology, operational) => {
+ if (name) {
+ dispatch(
+ addScenario({
+ portfolioId,
+ name,
+ trace,
+ topology,
+ operational,
+ })
+ )
+ }
+
+ dispatch(closeNewScenarioModal())
+ },
+ }
+}
+
+const NewScenarioModal = connect(mapStateToProps, mapDispatchToProps)(NewScenarioModalComponent)
+
+export default NewScenarioModal
diff --git a/opendc-web/opendc-web-ui/src/containers/modals/NewTopologyModal.js b/opendc-web/opendc-web-ui/src/containers/modals/NewTopologyModal.js
new file mode 100644
index 00000000..0acf6cf2
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/modals/NewTopologyModal.js
@@ -0,0 +1,42 @@
+import { connect } from 'react-redux'
+import NewTopologyModalComponent from '../../components/modals/custom-components/NewTopologyModalComponent'
+import { closeNewTopologyModal } from '../../actions/modals/topology'
+import { addTopology } from '../../actions/topologies'
+
+const mapStateToProps = (state) => {
+ let topologies = state.objects.project[state.currentProjectId]
+ ? state.objects.project[state.currentProjectId].topologyIds.map((t) => state.objects.topology[t])
+ : []
+ if (topologies.filter((t) => !t).length > 0) {
+ topologies = []
+ }
+
+ return {
+ show: state.modals.changeTopologyModalVisible,
+ topologies,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onCreateTopology: (name) => {
+ if (name) {
+ dispatch(addTopology(name, undefined))
+ }
+ dispatch(closeNewTopologyModal())
+ },
+ onDuplicateTopology: (name, id) => {
+ if (name) {
+ dispatch(addTopology(name, id))
+ }
+ dispatch(closeNewTopologyModal())
+ },
+ onCancel: () => {
+ dispatch(closeNewTopologyModal())
+ },
+ }
+}
+
+const NewTopologyModal = connect(mapStateToProps, mapDispatchToProps)(NewTopologyModalComponent)
+
+export default NewTopologyModal
diff --git a/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js b/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js
new file mode 100644
index 00000000..845d54e1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import AppNavbarComponent from '../../components/navigation/AppNavbarComponent'
+
+const mapStateToProps = (state) => {
+ return {
+ project: state.currentProjectId !== '-1' ? state.objects.project[state.currentProjectId] : undefined,
+ }
+}
+
+const AppNavbarContainer = connect(mapStateToProps)(AppNavbarComponent)
+
+export default AppNavbarContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/projects/FilterLink.js b/opendc-web/opendc-web-ui/src/containers/projects/FilterLink.js
new file mode 100644
index 00000000..dfd6affe
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/projects/FilterLink.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux'
+import { setAuthVisibilityFilter } from '../../actions/projects'
+import FilterButton from '../../components/projects/FilterButton'
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ active: state.projectList.authVisibilityFilter === ownProps.filter,
+ }
+}
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ onClick: () => dispatch(setAuthVisibilityFilter(ownProps.filter)),
+ }
+}
+
+const FilterLink = connect(mapStateToProps, mapDispatchToProps)(FilterButton)
+
+export default FilterLink
diff --git a/opendc-web/opendc-web-ui/src/containers/projects/NewProjectButtonContainer.js b/opendc-web/opendc-web-ui/src/containers/projects/NewProjectButtonContainer.js
new file mode 100644
index 00000000..ffd4a4a3
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/projects/NewProjectButtonContainer.js
@@ -0,0 +1,13 @@
+import { connect } from 'react-redux'
+import { openNewProjectModal } from '../../actions/modals/projects'
+import NewProjectButtonComponent from '../../components/projects/NewProjectButtonComponent'
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onClick: () => dispatch(openNewProjectModal()),
+ }
+}
+
+const NewProjectButtonContainer = connect(undefined, mapDispatchToProps)(NewProjectButtonComponent)
+
+export default NewProjectButtonContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js b/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js
new file mode 100644
index 00000000..8bcbb7fd
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import { deleteProject } from '../../actions/projects'
+import ProjectActionButtons from '../../components/projects/ProjectActionButtons'
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ projectId: ownProps.projectId,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onViewUsers: (id) => {}, // TODO implement user viewing
+ onDelete: (id) => dispatch(deleteProject(id)),
+ }
+}
+
+const ProjectActions = connect(mapStateToProps, mapDispatchToProps)(ProjectActionButtons)
+
+export default ProjectActions
diff --git a/opendc-web/opendc-web-ui/src/containers/projects/VisibleProjectAuthList.js b/opendc-web/opendc-web-ui/src/containers/projects/VisibleProjectAuthList.js
new file mode 100644
index 00000000..f0010540
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/containers/projects/VisibleProjectAuthList.js
@@ -0,0 +1,32 @@
+import { connect } from 'react-redux'
+import ProjectList from '../../components/projects/ProjectAuthList'
+
+const getVisibleProjectAuths = (projectAuths, filter) => {
+ switch (filter) {
+ case 'SHOW_ALL':
+ return projectAuths
+ case 'SHOW_OWN':
+ return projectAuths.filter((projectAuth) => projectAuth.authorizationLevel === 'OWN')
+ case 'SHOW_SHARED':
+ return projectAuths.filter((projectAuth) => projectAuth.authorizationLevel !== 'OWN')
+ default:
+ return projectAuths
+ }
+}
+
+const mapStateToProps = (state) => {
+ const denormalizedAuthorizations = state.projectList.authorizationsOfCurrentUser.map((authorizationIds) => {
+ const authorization = state.objects.authorization[authorizationIds]
+ authorization.user = state.objects.user[authorization.userId]
+ authorization.project = state.objects.project[authorization.projectId]
+ return authorization
+ })
+
+ return {
+ authorizations: getVisibleProjectAuths(denormalizedAuthorizations, state.projectList.authVisibilityFilter),
+ }
+}
+
+const VisibleProjectAuthList = connect(mapStateToProps)(ProjectList)
+
+export default VisibleProjectAuthList
diff --git a/opendc-web/opendc-web-ui/src/index.js b/opendc-web/opendc-web-ui/src/index.js
new file mode 100644
index 00000000..3517147e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/index.js
@@ -0,0 +1,30 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import * as Sentry from '@sentry/react'
+import { Integrations } from '@sentry/tracing'
+import { Provider } from 'react-redux'
+import { setupSocketConnection } from './api/socket'
+import './index.sass'
+import Routes from './routes'
+import configureStore from './store/configure-store'
+
+setupSocketConnection(() => {
+ const store = configureStore()
+
+ // Initialize Sentry if the user has configured a DSN
+ if (process.env.REACT_APP_SENTRY_DSN) {
+ Sentry.init({
+ environment: process.env.NODE_ENV,
+ dsn: process.env.REACT_APP_SENTRY_DSN,
+ integrations: [new Integrations.BrowserTracing()],
+ tracesSampleRate: 0.1,
+ })
+ }
+
+ ReactDOM.render(
+ <Provider store={store}>
+ <Routes />
+ </Provider>,
+ document.getElementById('root')
+ )
+})
diff --git a/opendc-web/opendc-web-ui/src/index.sass b/opendc-web/opendc-web-ui/src/index.sass
new file mode 100644
index 00000000..a78f7a19
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/index.sass
@@ -0,0 +1,52 @@
+@import "~bootstrap/scss/bootstrap"
+
+@import ./style-globals/_mixins.sass
+@import ./style-globals/_variables.sass
+
+html, body, #root
+ margin: 0
+ padding: 0
+ width: 100%
+ height: 100%
+
+ font-family: Roboto, Helvetica, Verdana, sans-serif
+ background: #eee
+
+ // Scroll padding for top navbar
+ scroll-padding-top: 60px
+
+.full-height
+ position: relative
+ height: 100% !important
+
+.page-container
+ padding-top: 60px
+
+.text-page-container
+ padding-top: 80px
+ display: flex
+ flex-flow: column
+
+.vertically-expanding-container
+ flex: 1 1 auto
+ overflow-y: auto
+
+.bottom-btn-container
+ flex: 0 1 auto
+ padding: 20px 0
+
+.btn, .list-group-item-action, .clickable
+ +clickable
+
+.btn-circle
+ +border-radius(50%)
+
+a, a:hover
+ text-decoration: none
+
+.app-page-container
+ padding-left: $side-bar-width
+ padding-top: 15px
+
+.w-70
+ width: 70% !important
diff --git a/opendc-web/opendc-web-ui/src/pages/App.js b/opendc-web/opendc-web-ui/src/pages/App.js
new file mode 100644
index 00000000..cbc805b8
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/App.js
@@ -0,0 +1,137 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import DocumentTitle from 'react-document-title'
+import { connect } from 'react-redux'
+import { ShortcutManager } from 'react-shortcuts'
+import { openPortfolioSucceeded } from '../actions/portfolios'
+import { openProjectSucceeded } from '../actions/projects'
+import ToolPanelComponent from '../components/app/map/controls/ToolPanelComponent'
+import LoadingScreen from '../components/app/map/LoadingScreen'
+import ScaleIndicatorContainer from '../containers/app/map/controls/ScaleIndicatorContainer'
+import MapStage from '../containers/app/map/MapStage'
+import TopologySidebarContainer from '../containers/app/sidebars/topology/TopologySidebarContainer'
+import DeleteMachineModal from '../containers/modals/DeleteMachineModal'
+import DeleteRackModal from '../containers/modals/DeleteRackModal'
+import DeleteRoomModal from '../containers/modals/DeleteRoomModal'
+import EditRackNameModal from '../containers/modals/EditRackNameModal'
+import EditRoomNameModal from '../containers/modals/EditRoomNameModal'
+import KeymapConfiguration from '../shortcuts/keymap'
+import NewTopologyModal from '../containers/modals/NewTopologyModal'
+import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
+import ProjectSidebarContainer from '../containers/app/sidebars/project/ProjectSidebarContainer'
+import { openScenarioSucceeded } from '../actions/scenarios'
+import NewPortfolioModal from '../containers/modals/NewPortfolioModal'
+import NewScenarioModal from '../containers/modals/NewScenarioModal'
+import PortfolioResultsContainer from '../containers/app/results/PortfolioResultsContainer'
+
+const shortcutManager = new ShortcutManager(KeymapConfiguration)
+
+class AppComponent extends React.Component {
+ static propTypes = {
+ projectId: PropTypes.string.isRequired,
+ portfolioId: PropTypes.string,
+ scenarioId: PropTypes.string,
+ projectName: PropTypes.string,
+ }
+ static childContextTypes = {
+ shortcuts: PropTypes.object.isRequired,
+ }
+
+ componentDidMount() {
+ if (this.props.scenarioId) {
+ this.props.openScenarioSucceeded(this.props.projectId, this.props.portfolioId, this.props.scenarioId)
+ } else if (this.props.portfolioId) {
+ this.props.openPortfolioSucceeded(this.props.projectId, this.props.portfolioId)
+ } else {
+ this.props.openProjectSucceeded(this.props.projectId)
+ }
+ }
+
+ getChildContext() {
+ return {
+ shortcuts: shortcutManager,
+ }
+ }
+
+ render() {
+ const constructionElements = this.props.topologyIsLoading ? (
+ <div className="full-height d-flex align-items-center justify-content-center">
+ <LoadingScreen />
+ </div>
+ ) : (
+ <div className="full-height">
+ <MapStage />
+ <ScaleIndicatorContainer />
+ <ToolPanelComponent />
+ <ProjectSidebarContainer />
+ <TopologySidebarContainer />
+ </div>
+ )
+
+ const portfolioElements = (
+ <div className="full-height app-page-container">
+ <ProjectSidebarContainer />
+ <div className="container-fluid full-height">
+ <PortfolioResultsContainer />
+ </div>
+ </div>
+ )
+
+ const scenarioElements = (
+ <div className="full-height app-page-container">
+ <ProjectSidebarContainer />
+ <div className="container-fluid full-height">
+ <h2>Scenario loading</h2>
+ </div>
+ </div>
+ )
+
+ return (
+ <DocumentTitle
+ title={this.props.projectName ? this.props.projectName + ' - OpenDC' : 'Simulation - OpenDC'}
+ >
+ <div className="page-container full-height">
+ <AppNavbarContainer fullWidth={true} />
+ {this.props.scenarioId
+ ? scenarioElements
+ : this.props.portfolioId
+ ? portfolioElements
+ : constructionElements}
+ <NewTopologyModal />
+ <NewPortfolioModal />
+ <NewScenarioModal />
+ <EditRoomNameModal />
+ <DeleteRoomModal />
+ <EditRackNameModal />
+ <DeleteRackModal />
+ <DeleteMachineModal />
+ </div>
+ </DocumentTitle>
+ )
+ }
+}
+
+const mapStateToProps = (state) => {
+ let projectName = undefined
+ if (state.currentProjectId !== '-1' && state.objects.project[state.currentProjectId]) {
+ projectName = state.objects.project[state.currentProjectId].name
+ }
+
+ return {
+ topologyIsLoading: state.currentTopologyId === '-1',
+ projectName,
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ openProjectSucceeded: (projectId) => dispatch(openProjectSucceeded(projectId)),
+ openPortfolioSucceeded: (projectId, portfolioId) => dispatch(openPortfolioSucceeded(projectId, portfolioId)),
+ openScenarioSucceeded: (projectId, portfolioId, scenarioId) =>
+ dispatch(openScenarioSucceeded(projectId, portfolioId, scenarioId)),
+ }
+}
+
+const App = connect(mapStateToProps, mapDispatchToProps)(AppComponent)
+
+export default App
diff --git a/opendc-web/opendc-web-ui/src/pages/Home.js b/opendc-web/opendc-web-ui/src/pages/Home.js
new file mode 100644
index 00000000..6fc940c0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/Home.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import DocumentTitle from 'react-document-title'
+import ContactSection from '../components/home/ContactSection'
+import IntroSection from '../components/home/IntroSection'
+import JumbotronHeader from '../components/home/JumbotronHeader'
+import ModelingSection from '../components/home/ModelingSection'
+import SimulationSection from '../components/home/SimulationSection'
+import StakeholderSection from '../components/home/StakeholderSection'
+import TeamSection from '../components/home/TeamSection'
+import TechnologiesSection from '../components/home/TechnologiesSection'
+import HomeNavbar from '../components/navigation/HomeNavbar'
+import './Home.sass'
+
+function Home() {
+ return (
+ <div>
+ <HomeNavbar />
+ <div className="body-wrapper page-container">
+ <JumbotronHeader />
+ <IntroSection />
+ <StakeholderSection />
+ <ModelingSection />
+ <SimulationSection />
+ <TechnologiesSection />
+ <TeamSection />
+ <ContactSection />
+ <DocumentTitle title="OpenDC" />
+ </div>
+ </div>
+ )
+}
+
+export default Home
diff --git a/opendc-web/opendc-web-ui/src/pages/Home.sass b/opendc-web/opendc-web-ui/src/pages/Home.sass
new file mode 100644
index 00000000..79cb9698
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/Home.sass
@@ -0,0 +1,9 @@
+.body-wrapper
+ position: relative
+ overflow-y: hidden
+
+.intro-section, .modeling-section, .technologies-section
+ background-color: #fff
+
+.stakeholder-section, .simulation-section, .team-section
+ background-color: #f2f2f2
diff --git a/opendc-web/opendc-web-ui/src/pages/NotFound.js b/opendc-web/opendc-web-ui/src/pages/NotFound.js
new file mode 100644
index 00000000..72be7342
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/NotFound.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import DocumentTitle from 'react-document-title'
+import TerminalWindow from '../components/not-found/TerminalWindow'
+import './NotFound.sass'
+
+const NotFound = () => (
+ <DocumentTitle title="Page Not Found - OpenDC">
+ <div className="not-found-backdrop">
+ <TerminalWindow />
+ </div>
+ </DocumentTitle>
+)
+
+export default NotFound
diff --git a/opendc-web/opendc-web-ui/src/pages/NotFound.sass b/opendc-web/opendc-web-ui/src/pages/NotFound.sass
new file mode 100644
index 00000000..59231f7a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/NotFound.sass
@@ -0,0 +1,11 @@
+.not-found-backdrop
+ position: absolute
+ left: 0
+ top: 0
+
+ margin: 0
+ padding: 0
+ width: 100%
+ height: 100%
+
+ background-image: linear-gradient(135deg, #00678a, #008fbf, #00A6D6)
diff --git a/opendc-web/opendc-web-ui/src/pages/Profile.js b/opendc-web/opendc-web-ui/src/pages/Profile.js
new file mode 100644
index 00000000..0d94b519
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/Profile.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import DocumentTitle from 'react-document-title'
+import { connect } from 'react-redux'
+import { openDeleteProfileModal } from '../actions/modals/profile'
+import DeleteProfileModal from '../containers/modals/DeleteProfileModal'
+import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
+
+const ProfileContainer = ({ onDelete }) => (
+ <DocumentTitle title="My Profile - OpenDC">
+ <div className="full-height">
+ <AppNavbarContainer fullWidth={false} />
+ <div className="container text-page-container full-height">
+ <button className="btn btn-danger mb-2 ml-auto mr-auto" style={{ maxWidth: 300 }} onClick={onDelete}>
+ Delete my account on OpenDC
+ </button>
+ <p className="text-muted text-center">
+ This does not delete your Google account, but simply disconnects it from the OpenDC platform and
+ deletes any project info that is associated with you (projects you own and any authorizations you
+ may have on other projects).
+ </p>
+ </div>
+ <DeleteProfileModal />
+ </div>
+ </DocumentTitle>
+)
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onDelete: () => dispatch(openDeleteProfileModal()),
+ }
+}
+
+const Profile = connect(undefined, mapDispatchToProps)(ProfileContainer)
+
+export default Profile
diff --git a/opendc-web/opendc-web-ui/src/pages/Projects.js b/opendc-web/opendc-web-ui/src/pages/Projects.js
new file mode 100644
index 00000000..bb54aaa5
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/Projects.js
@@ -0,0 +1,43 @@
+import React from 'react'
+import DocumentTitle from 'react-document-title'
+import { connect } from 'react-redux'
+import { openNewProjectModal } from '../actions/modals/projects'
+import { fetchAuthorizationsOfCurrentUser } from '../actions/users'
+import ProjectFilterPanel from '../components/projects/FilterPanel'
+import NewProjectModal from '../containers/modals/NewProjectModal'
+import NewProjectButtonContainer from '../containers/projects/NewProjectButtonContainer'
+import VisibleProjectList from '../containers/projects/VisibleProjectAuthList'
+import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
+
+class ProjectsContainer extends React.Component {
+ componentDidMount() {
+ this.props.fetchAuthorizationsOfCurrentUser()
+ }
+
+ render() {
+ return (
+ <DocumentTitle title="My Projects - OpenDC">
+ <div className="full-height">
+ <AppNavbarContainer fullWidth={false} />
+ <div className="container text-page-container full-height">
+ <ProjectFilterPanel />
+ <VisibleProjectList />
+ <NewProjectButtonContainer />
+ </div>
+ <NewProjectModal />
+ </div>
+ </DocumentTitle>
+ )
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ fetchAuthorizationsOfCurrentUser: () => dispatch(fetchAuthorizationsOfCurrentUser()),
+ openNewProjectModal: () => dispatch(openNewProjectModal()),
+ }
+}
+
+const Projects = connect(undefined, mapDispatchToProps)(ProjectsContainer)
+
+export default Projects
diff --git a/opendc-web/opendc-web-ui/src/reducers/auth.js b/opendc-web/opendc-web-ui/src/reducers/auth.js
new file mode 100644
index 00000000..399a4b10
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/reducers/auth.js
@@ -0,0 +1,12 @@
+import { LOG_IN_SUCCEEDED, LOG_OUT } from '../actions/auth'
+
+export function auth(state = {}, action) {
+ switch (action.type) {
+ case LOG_IN_SUCCEEDED:
+ return action.payload
+ case LOG_OUT:
+ return {}
+ default:
+ return state
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/reducers/construction-mode.js b/opendc-web/opendc-web-ui/src/reducers/construction-mode.js
new file mode 100644
index 00000000..257dddd2
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/reducers/construction-mode.js
@@ -0,0 +1,52 @@
+import { combineReducers } from 'redux'
+import { GO_DOWN_ONE_INTERACTION_LEVEL } from '../actions/interaction-level'
+import {
+ CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED,
+ FINISH_NEW_ROOM_CONSTRUCTION,
+ FINISH_ROOM_EDIT,
+ SET_CURRENT_TOPOLOGY,
+ START_NEW_ROOM_CONSTRUCTION_SUCCEEDED,
+ START_ROOM_EDIT,
+} from '../actions/topology/building'
+import { DELETE_ROOM, START_RACK_CONSTRUCTION, STOP_RACK_CONSTRUCTION } from '../actions/topology/room'
+import { OPEN_PORTFOLIO_SUCCEEDED } from '../actions/portfolios'
+import { OPEN_SCENARIO_SUCCEEDED } from '../actions/scenarios'
+
+export function currentRoomInConstruction(state = '-1', action) {
+ switch (action.type) {
+ case START_NEW_ROOM_CONSTRUCTION_SUCCEEDED:
+ return action.roomId
+ case START_ROOM_EDIT:
+ return action.roomId
+ case CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED:
+ case FINISH_NEW_ROOM_CONSTRUCTION:
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case OPEN_SCENARIO_SUCCEEDED:
+ case FINISH_ROOM_EDIT:
+ case SET_CURRENT_TOPOLOGY:
+ case DELETE_ROOM:
+ return '-1'
+ default:
+ return state
+ }
+}
+
+export function inRackConstructionMode(state = false, action) {
+ switch (action.type) {
+ case START_RACK_CONSTRUCTION:
+ return true
+ case STOP_RACK_CONSTRUCTION:
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case OPEN_SCENARIO_SUCCEEDED:
+ case SET_CURRENT_TOPOLOGY:
+ case GO_DOWN_ONE_INTERACTION_LEVEL:
+ return false
+ default:
+ return state
+ }
+}
+
+export const construction = combineReducers({
+ currentRoomInConstruction,
+ inRackConstructionMode,
+})
diff --git a/opendc-web/opendc-web-ui/src/reducers/current-ids.js b/opendc-web/opendc-web-ui/src/reducers/current-ids.js
new file mode 100644
index 00000000..9b46aa60
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/reducers/current-ids.js
@@ -0,0 +1,54 @@
+import { OPEN_PORTFOLIO_SUCCEEDED, SET_CURRENT_PORTFOLIO } from '../actions/portfolios'
+import { OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
+import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building'
+import { OPEN_SCENARIO_SUCCEEDED, SET_CURRENT_SCENARIO } from '../actions/scenarios'
+
+export function currentTopologyId(state = '-1', action) {
+ switch (action.type) {
+ case SET_CURRENT_TOPOLOGY:
+ return action.topologyId
+ default:
+ return state
+ }
+}
+
+export function currentProjectId(state = '-1', action) {
+ switch (action.type) {
+ case OPEN_PROJECT_SUCCEEDED:
+ return action.id
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case OPEN_SCENARIO_SUCCEEDED:
+ return action.projectId
+ default:
+ return state
+ }
+}
+
+export function currentPortfolioId(state = '-1', action) {
+ switch (action.type) {
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case SET_CURRENT_PORTFOLIO:
+ case SET_CURRENT_SCENARIO:
+ return action.portfolioId
+ case OPEN_SCENARIO_SUCCEEDED:
+ return action.portfolioId
+ case OPEN_PROJECT_SUCCEEDED:
+ case SET_CURRENT_TOPOLOGY:
+ return '-1'
+ default:
+ return state
+ }
+}
+export function currentScenarioId(state = '-1', action) {
+ switch (action.type) {
+ case OPEN_SCENARIO_SUCCEEDED:
+ case SET_CURRENT_SCENARIO:
+ return action.scenarioId
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case SET_CURRENT_TOPOLOGY:
+ case OPEN_PROJECT_SUCCEEDED:
+ return '-1'
+ default:
+ return state
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/reducers/index.js b/opendc-web/opendc-web-ui/src/reducers/index.js
new file mode 100644
index 00000000..787d5a74
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/reducers/index.js
@@ -0,0 +1,25 @@
+import { combineReducers } from 'redux'
+import { auth } from './auth'
+import { construction } from './construction-mode'
+import { currentPortfolioId, currentProjectId, currentScenarioId, currentTopologyId } from './current-ids'
+import { interactionLevel } from './interaction-level'
+import { map } from './map'
+import { modals } from './modals'
+import { objects } from './objects'
+import { projectList } from './project-list'
+
+const rootReducer = combineReducers({
+ objects,
+ modals,
+ projectList,
+ construction,
+ map,
+ currentProjectId,
+ currentTopologyId,
+ currentPortfolioId,
+ currentScenarioId,
+ interactionLevel,
+ auth,
+})
+
+export default rootReducer
diff --git a/opendc-web/opendc-web-ui/src/reducers/interaction-level.js b/opendc-web/opendc-web-ui/src/reducers/interaction-level.js
new file mode 100644
index 00000000..eafcb269
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/reducers/interaction-level.js
@@ -0,0 +1,61 @@
+import { OPEN_PORTFOLIO_SUCCEEDED } from '../actions/portfolios'
+import {
+ GO_DOWN_ONE_INTERACTION_LEVEL,
+ GO_FROM_BUILDING_TO_ROOM,
+ GO_FROM_RACK_TO_MACHINE,
+ GO_FROM_ROOM_TO_RACK,
+} from '../actions/interaction-level'
+import { OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
+import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building'
+import { OPEN_SCENARIO_SUCCEEDED } from '../actions/scenarios'
+
+export function interactionLevel(state = { mode: 'BUILDING' }, action) {
+ switch (action.type) {
+ case OPEN_PORTFOLIO_SUCCEEDED:
+ case OPEN_SCENARIO_SUCCEEDED:
+ case OPEN_PROJECT_SUCCEEDED:
+ case SET_CURRENT_TOPOLOGY:
+ return {
+ mode: 'BUILDING',
+ }
+ case GO_FROM_BUILDING_TO_ROOM:
+ return {
+ mode: 'ROOM',
+ roomId: action.roomId,
+ }
+ case GO_FROM_ROOM_TO_RACK:
+ return {
+ mode: 'RACK',
+ roomId: state.roomId,
+ tileId: action.tileId,
+ }
+ case GO_FROM_RACK_TO_MACHINE:
+ return {
+ mode: 'MACHINE',
+ roomId: state.roomId,
+ tileId: state.tileId,
+ position: action.position,
+ }
+ case GO_DOWN_ONE_INTERACTION_LEVEL:
+ if (state.mode === 'ROOM') {
+ return {
+ mode: 'BUILDING',
+ }
+ } else if (state.mode === 'RACK') {
+ return {
+ mode: 'ROOM',
+ roomId: state.roomId,
+ }
+ } else if (state.mode === 'MACHINE') {
+ return {
+ mode: 'RACK',
+ roomId: state.roomId,
+ tileId: state.tileId,
+ }
+ } else {
+ return state
+ }
+ default:
+ return state
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/reducers/map.js b/opendc-web/opendc-web-ui/src/reducers/map.js
new file mode 100644
index 00000000..de712c15
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/reducers/map.js
@@ -0,0 +1,35 @@
+import { combineReducers } from 'redux'
+import { SET_MAP_DIMENSIONS, SET_MAP_POSITION, SET_MAP_SCALE } from '../actions/map'
+
+export function position(state = { x: 0, y: 0 }, action) {
+ switch (action.type) {
+ case SET_MAP_POSITION:
+ return { x: action.x, y: action.y }
+ default:
+ return state
+ }
+}
+
+export function dimensions(state = { width: 600, height: 400 }, action) {
+ switch (action.type) {
+ case SET_MAP_DIMENSIONS:
+ return { width: action.width, height: action.height }
+ default:
+ return state
+ }
+}
+
+export function scale(state = 1, action) {
+ switch (action.type) {
+ case SET_MAP_SCALE:
+ return action.scale
+ default:
+ return state
+ }
+}
+
+export const map = combineReducers({
+ position,
+ dimensions,
+ scale,
+})
diff --git a/opendc-web/opendc-web-ui/src/reducers/modals.js b/opendc-web/opendc-web-ui/src/reducers/modals.js
new file mode 100644
index 00000000..a7656373
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/reducers/modals.js
@@ -0,0 +1,45 @@
+import { combineReducers } from 'redux'
+import { CLOSE_DELETE_PROFILE_MODAL, OPEN_DELETE_PROFILE_MODAL } from '../actions/modals/profile'
+import { CLOSE_NEW_PROJECT_MODAL, OPEN_NEW_PROJECT_MODAL } from '../actions/modals/projects'
+import {
+ CLOSE_NEW_TOPOLOGY_MODAL,
+ CLOSE_DELETE_MACHINE_MODAL,
+ CLOSE_DELETE_RACK_MODAL,
+ CLOSE_DELETE_ROOM_MODAL,
+ CLOSE_EDIT_RACK_NAME_MODAL,
+ CLOSE_EDIT_ROOM_NAME_MODAL,
+ OPEN_NEW_TOPOLOGY_MODAL,
+ OPEN_DELETE_MACHINE_MODAL,
+ OPEN_DELETE_RACK_MODAL,
+ OPEN_DELETE_ROOM_MODAL,
+ OPEN_EDIT_RACK_NAME_MODAL,
+ OPEN_EDIT_ROOM_NAME_MODAL,
+} from '../actions/modals/topology'
+import { CLOSE_NEW_PORTFOLIO_MODAL, OPEN_NEW_PORTFOLIO_MODAL } from '../actions/modals/portfolios'
+import { CLOSE_NEW_SCENARIO_MODAL, OPEN_NEW_SCENARIO_MODAL } from '../actions/modals/scenarios'
+
+function modal(openAction, closeAction) {
+ return function (state = false, action) {
+ switch (action.type) {
+ case openAction:
+ return true
+ case closeAction:
+ return false
+ default:
+ return state
+ }
+ }
+}
+
+export const modals = combineReducers({
+ newProjectModalVisible: modal(OPEN_NEW_PROJECT_MODAL, CLOSE_NEW_PROJECT_MODAL),
+ deleteProfileModalVisible: modal(OPEN_DELETE_PROFILE_MODAL, CLOSE_DELETE_PROFILE_MODAL),
+ changeTopologyModalVisible: modal(OPEN_NEW_TOPOLOGY_MODAL, CLOSE_NEW_TOPOLOGY_MODAL),
+ editRoomNameModalVisible: modal(OPEN_EDIT_ROOM_NAME_MODAL, CLOSE_EDIT_ROOM_NAME_MODAL),
+ deleteRoomModalVisible: modal(OPEN_DELETE_ROOM_MODAL, CLOSE_DELETE_ROOM_MODAL),
+ editRackNameModalVisible: modal(OPEN_EDIT_RACK_NAME_MODAL, CLOSE_EDIT_RACK_NAME_MODAL),
+ deleteRackModalVisible: modal(OPEN_DELETE_RACK_MODAL, CLOSE_DELETE_RACK_MODAL),
+ deleteMachineModalVisible: modal(OPEN_DELETE_MACHINE_MODAL, CLOSE_DELETE_MACHINE_MODAL),
+ newPortfolioModalVisible: modal(OPEN_NEW_PORTFOLIO_MODAL, CLOSE_NEW_PORTFOLIO_MODAL),
+ newScenarioModalVisible: modal(OPEN_NEW_SCENARIO_MODAL, CLOSE_NEW_SCENARIO_MODAL),
+})
diff --git a/opendc-web/opendc-web-ui/src/reducers/objects.js b/opendc-web/opendc-web-ui/src/reducers/objects.js
new file mode 100644
index 00000000..1f721b2e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/reducers/objects.js
@@ -0,0 +1,64 @@
+import { combineReducers } from 'redux'
+import {
+ ADD_ID_TO_STORE_OBJECT_LIST_PROP,
+ ADD_PROP_TO_STORE_OBJECT,
+ ADD_TO_STORE,
+ REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP,
+} from '../actions/objects'
+import { CPU_UNITS, GPU_UNITS, MEMORY_UNITS, STORAGE_UNITS } from '../util/unit-specifications'
+
+export const objects = combineReducers({
+ project: object('project'),
+ user: object('user'),
+ authorization: objectWithId('authorization', (object) => [object.userId, object.projectId]),
+ cpu: object('cpu', CPU_UNITS),
+ gpu: object('gpu', GPU_UNITS),
+ memory: object('memory', MEMORY_UNITS),
+ storage: object('storage', STORAGE_UNITS),
+ machine: object('machine'),
+ rack: object('rack'),
+ tile: object('tile'),
+ room: object('room'),
+ topology: object('topology'),
+ trace: object('trace'),
+ scheduler: object('scheduler'),
+ portfolio: object('portfolio'),
+ scenario: object('scenario'),
+ prefab: object('prefab'),
+})
+
+function object(type, defaultState = {}) {
+ return objectWithId(type, (object) => object._id, defaultState)
+}
+
+function objectWithId(type, getId, defaultState = {}) {
+ return (state = defaultState, action) => {
+ if (action.objectType !== type) {
+ return state
+ }
+
+ if (action.type === ADD_TO_STORE) {
+ return Object.assign({}, state, {
+ [getId(action.object)]: action.object,
+ })
+ } else if (action.type === ADD_PROP_TO_STORE_OBJECT) {
+ return Object.assign({}, state, {
+ [action.objectId]: Object.assign({}, state[action.objectId], action.propObject),
+ })
+ } else if (action.type === ADD_ID_TO_STORE_OBJECT_LIST_PROP) {
+ return Object.assign({}, state, {
+ [action.objectId]: Object.assign({}, state[action.objectId], {
+ [action.propName]: [...state[action.objectId][action.propName], action.id],
+ }),
+ })
+ } else if (action.type === REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP) {
+ return Object.assign({}, state, {
+ [action.objectId]: Object.assign({}, state[action.objectId], {
+ [action.propName]: state[action.objectId][action.propName].filter((id) => id !== action.id),
+ }),
+ })
+ }
+
+ return state
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/reducers/project-list.js b/opendc-web/opendc-web-ui/src/reducers/project-list.js
new file mode 100644
index 00000000..1f1aa8d0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/reducers/project-list.js
@@ -0,0 +1,30 @@
+import { combineReducers } from 'redux'
+import { ADD_PROJECT_SUCCEEDED, DELETE_PROJECT_SUCCEEDED, SET_AUTH_VISIBILITY_FILTER } from '../actions/projects'
+import { FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED } from '../actions/users'
+
+export function authorizationsOfCurrentUser(state = [], action) {
+ switch (action.type) {
+ case FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED:
+ return action.authorizationsOfCurrentUser
+ case ADD_PROJECT_SUCCEEDED:
+ return [...state, action.authorization]
+ case DELETE_PROJECT_SUCCEEDED:
+ return state.filter((authorization) => authorization[1] !== action.id)
+ default:
+ return state
+ }
+}
+
+export function authVisibilityFilter(state = 'SHOW_ALL', action) {
+ switch (action.type) {
+ case SET_AUTH_VISIBILITY_FILTER:
+ return action.filter
+ default:
+ return state
+ }
+}
+
+export const projectList = combineReducers({
+ authorizationsOfCurrentUser,
+ authVisibilityFilter,
+})
diff --git a/opendc-web/opendc-web-ui/src/routes/index.js b/opendc-web/opendc-web-ui/src/routes/index.js
new file mode 100644
index 00000000..4291a046
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/routes/index.js
@@ -0,0 +1,40 @@
+import React from 'react'
+import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
+import { userIsLoggedIn } from '../auth/index'
+import App from '../pages/App'
+import Home from '../pages/Home'
+import NotFound from '../pages/NotFound'
+import Profile from '../pages/Profile'
+import Projects from '../pages/Projects'
+
+const ProtectedComponent = (component) => () => (userIsLoggedIn() ? component : <Redirect to="/" />)
+const AppComponent = ({ match }) =>
+ userIsLoggedIn() ? (
+ <App
+ projectId={match.params.projectId}
+ portfolioId={match.params.portfolioId}
+ scenarioId={match.params.scenarioId}
+ />
+ ) : (
+ <Redirect to="/" />
+ )
+
+const Routes = () => (
+ <BrowserRouter>
+ <Switch>
+ <Route exact path="/" component={Home} />
+ <Route exact path="/projects" render={ProtectedComponent(<Projects />)} />
+ <Route exact path="/projects/:projectId" component={AppComponent} />
+ <Route exact path="/projects/:projectId/portfolios/:portfolioId" component={AppComponent} />
+ <Route
+ exact
+ path="/projects/:projectId/portfolios/:portfolioId/scenarios/:scenarioId"
+ component={AppComponent}
+ />
+ <Route exact path="/profile" render={ProtectedComponent(<Profile />)} />
+ <Route path="/*" component={NotFound} />
+ </Switch>
+ </BrowserRouter>
+)
+
+export default Routes
diff --git a/opendc-web/opendc-web-ui/src/sagas/index.js b/opendc-web/opendc-web-ui/src/sagas/index.js
new file mode 100644
index 00000000..6332b2fb
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/index.js
@@ -0,0 +1,80 @@
+import { takeEvery } from 'redux-saga/effects'
+import { LOG_IN } from '../actions/auth'
+import { ADD_PORTFOLIO, DELETE_PORTFOLIO, OPEN_PORTFOLIO_SUCCEEDED, UPDATE_PORTFOLIO } from '../actions/portfolios'
+import { ADD_PROJECT, DELETE_PROJECT, OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
+import {
+ ADD_TILE,
+ CANCEL_NEW_ROOM_CONSTRUCTION,
+ DELETE_TILE,
+ START_NEW_ROOM_CONSTRUCTION,
+} from '../actions/topology/building'
+import { ADD_UNIT, DELETE_MACHINE, DELETE_UNIT } from '../actions/topology/machine'
+import { ADD_MACHINE, DELETE_RACK, EDIT_RACK_NAME } from '../actions/topology/rack'
+import { ADD_RACK_TO_TILE, DELETE_ROOM, EDIT_ROOM_NAME } from '../actions/topology/room'
+import { DELETE_CURRENT_USER, FETCH_AUTHORIZATIONS_OF_CURRENT_USER } from '../actions/users'
+import { onAddPortfolio, onDeletePortfolio, onOpenPortfolioSucceeded, onUpdatePortfolio } from './portfolios'
+import { onDeleteCurrentUser } from './profile'
+import { onOpenProjectSucceeded, onProjectAdd, onProjectDelete } from './projects'
+import {
+ onAddMachine,
+ onAddRackToTile,
+ onAddTile,
+ onAddTopology,
+ onAddUnit,
+ onCancelNewRoomConstruction,
+ onDeleteMachine,
+ onDeleteRack,
+ onDeleteRoom,
+ onDeleteTile,
+ onDeleteTopology,
+ onDeleteUnit,
+ onEditRackName,
+ onEditRoomName,
+ onStartNewRoomConstruction,
+} from './topology'
+import { onFetchAuthorizationsOfCurrentUser, onFetchLoggedInUser } from './users'
+import { ADD_TOPOLOGY, DELETE_TOPOLOGY } from '../actions/topologies'
+import { ADD_SCENARIO, DELETE_SCENARIO, OPEN_SCENARIO_SUCCEEDED, UPDATE_SCENARIO } from '../actions/scenarios'
+import { onAddScenario, onDeleteScenario, onOpenScenarioSucceeded, onUpdateScenario } from './scenarios'
+import { onAddPrefab } from './prefabs'
+import { ADD_PREFAB } from '../actions/prefabs'
+
+export default function* rootSaga() {
+ yield takeEvery(LOG_IN, onFetchLoggedInUser)
+
+ yield takeEvery(FETCH_AUTHORIZATIONS_OF_CURRENT_USER, onFetchAuthorizationsOfCurrentUser)
+ yield takeEvery(ADD_PROJECT, onProjectAdd)
+ yield takeEvery(DELETE_PROJECT, onProjectDelete)
+
+ yield takeEvery(DELETE_CURRENT_USER, onDeleteCurrentUser)
+
+ yield takeEvery(OPEN_PROJECT_SUCCEEDED, onOpenProjectSucceeded)
+ yield takeEvery(OPEN_PORTFOLIO_SUCCEEDED, onOpenPortfolioSucceeded)
+ yield takeEvery(OPEN_SCENARIO_SUCCEEDED, onOpenScenarioSucceeded)
+
+ yield takeEvery(ADD_TOPOLOGY, onAddTopology)
+ yield takeEvery(DELETE_TOPOLOGY, onDeleteTopology)
+ yield takeEvery(START_NEW_ROOM_CONSTRUCTION, onStartNewRoomConstruction)
+ yield takeEvery(CANCEL_NEW_ROOM_CONSTRUCTION, onCancelNewRoomConstruction)
+ yield takeEvery(ADD_TILE, onAddTile)
+ yield takeEvery(DELETE_TILE, onDeleteTile)
+ yield takeEvery(EDIT_ROOM_NAME, onEditRoomName)
+ yield takeEvery(DELETE_ROOM, onDeleteRoom)
+ yield takeEvery(EDIT_RACK_NAME, onEditRackName)
+ yield takeEvery(DELETE_RACK, onDeleteRack)
+ yield takeEvery(ADD_RACK_TO_TILE, onAddRackToTile)
+ yield takeEvery(ADD_MACHINE, onAddMachine)
+ yield takeEvery(DELETE_MACHINE, onDeleteMachine)
+ yield takeEvery(ADD_UNIT, onAddUnit)
+ yield takeEvery(DELETE_UNIT, onDeleteUnit)
+
+ yield takeEvery(ADD_PORTFOLIO, onAddPortfolio)
+ yield takeEvery(UPDATE_PORTFOLIO, onUpdatePortfolio)
+ yield takeEvery(DELETE_PORTFOLIO, onDeletePortfolio)
+
+ yield takeEvery(ADD_SCENARIO, onAddScenario)
+ yield takeEvery(UPDATE_SCENARIO, onUpdateScenario)
+ yield takeEvery(DELETE_SCENARIO, onDeleteScenario)
+
+ yield takeEvery(ADD_PREFAB, onAddPrefab)
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/objects.js b/opendc-web/opendc-web-ui/src/sagas/objects.js
new file mode 100644
index 00000000..313d9976
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/objects.js
@@ -0,0 +1,229 @@
+import { call, put, select } from 'redux-saga/effects'
+import { addToStore } from '../actions/objects'
+import { getAllSchedulers } from '../api/routes/schedulers'
+import { getProject } from '../api/routes/projects'
+import { getAllTraces } from '../api/routes/traces'
+import { getUser } from '../api/routes/users'
+import { getTopology, updateTopology } from '../api/routes/topologies'
+import { uuid } from 'uuidv4'
+
+export const OBJECT_SELECTORS = {
+ project: (state) => state.objects.project,
+ user: (state) => state.objects.user,
+ authorization: (state) => state.objects.authorization,
+ portfolio: (state) => state.objects.portfolio,
+ scenario: (state) => state.objects.scenario,
+ cpu: (state) => state.objects.cpu,
+ gpu: (state) => state.objects.gpu,
+ memory: (state) => state.objects.memory,
+ storage: (state) => state.objects.storage,
+ machine: (state) => state.objects.machine,
+ rack: (state) => state.objects.rack,
+ tile: (state) => state.objects.tile,
+ room: (state) => state.objects.room,
+ topology: (state) => state.objects.topology,
+}
+
+function* fetchAndStoreObject(objectType, id, apiCall) {
+ const objectStore = yield select(OBJECT_SELECTORS[objectType])
+ let object = objectStore[id]
+ if (!object) {
+ object = yield apiCall
+ yield put(addToStore(objectType, object))
+ }
+ return object
+}
+
+function* fetchAndStoreObjects(objectType, apiCall) {
+ const objects = yield apiCall
+ for (let object of objects) {
+ yield put(addToStore(objectType, object))
+ }
+ return objects
+}
+
+export const fetchAndStoreProject = (id) => fetchAndStoreObject('project', id, call(getProject, id))
+
+export const fetchAndStoreUser = (id) => fetchAndStoreObject('user', id, call(getUser, id))
+
+export const fetchAndStoreTopology = function* (id) {
+ const topologyStore = yield select(OBJECT_SELECTORS['topology'])
+ const roomStore = yield select(OBJECT_SELECTORS['room'])
+ const tileStore = yield select(OBJECT_SELECTORS['tile'])
+ const rackStore = yield select(OBJECT_SELECTORS['rack'])
+ const machineStore = yield select(OBJECT_SELECTORS['machine'])
+
+ let topology = topologyStore[id]
+ if (!topology) {
+ const fullTopology = yield call(getTopology, id)
+
+ for (let roomIdx in fullTopology.rooms) {
+ const fullRoom = fullTopology.rooms[roomIdx]
+
+ generateIdIfNotPresent(fullRoom)
+
+ if (!roomStore[fullRoom._id]) {
+ for (let tileIdx in fullRoom.tiles) {
+ const fullTile = fullRoom.tiles[tileIdx]
+
+ generateIdIfNotPresent(fullTile)
+
+ if (!tileStore[fullTile._id]) {
+ if (fullTile.rack) {
+ const fullRack = fullTile.rack
+
+ generateIdIfNotPresent(fullRack)
+
+ if (!rackStore[fullRack._id]) {
+ for (let machineIdx in fullRack.machines) {
+ const fullMachine = fullRack.machines[machineIdx]
+
+ generateIdIfNotPresent(fullMachine)
+
+ if (!machineStore[fullMachine._id]) {
+ let machine = (({ _id, position, cpus, gpus, memories, storages }) => ({
+ _id,
+ rackId: fullRack._id,
+ position,
+ cpuIds: cpus.map((u) => u._id),
+ gpuIds: gpus.map((u) => u._id),
+ memoryIds: memories.map((u) => u._id),
+ storageIds: storages.map((u) => u._id),
+ }))(fullMachine)
+ yield put(addToStore('machine', machine))
+ }
+ }
+
+ const filledSlots = new Array(fullRack.capacity).fill(null)
+ fullRack.machines.forEach(
+ (machine) => (filledSlots[machine.position - 1] = machine._id)
+ )
+ let rack = (({ _id, name, capacity, powerCapacityW }) => ({
+ _id,
+ name,
+ capacity,
+ powerCapacityW,
+ machineIds: filledSlots,
+ }))(fullRack)
+ yield put(addToStore('rack', rack))
+ }
+ }
+
+ let tile = (({ _id, positionX, positionY, rack }) => ({
+ _id,
+ roomId: fullRoom._id,
+ positionX,
+ positionY,
+ rackId: rack ? rack._id : undefined,
+ }))(fullTile)
+ yield put(addToStore('tile', tile))
+ }
+ }
+
+ let room = (({ _id, name, tiles }) => ({ _id, name, tileIds: tiles.map((t) => t._id) }))(fullRoom)
+ yield put(addToStore('room', room))
+ }
+ }
+
+ topology = (({ _id, name, rooms }) => ({ _id, name, roomIds: rooms.map((r) => r._id) }))(fullTopology)
+ yield put(addToStore('topology', topology))
+
+ // TODO consider pushing the IDs
+ }
+
+ return topology
+}
+
+const generateIdIfNotPresent = (obj) => {
+ if (!obj._id) {
+ obj._id = uuid()
+ }
+}
+
+export const updateTopologyOnServer = function* (id) {
+ const topology = yield getTopologyAsObject(id, true)
+
+ yield call(updateTopology, topology)
+}
+
+export const getTopologyAsObject = function* (id, keepIds) {
+ const topologyStore = yield select(OBJECT_SELECTORS['topology'])
+ const rooms = yield getAllRooms(topologyStore[id].roomIds, keepIds)
+ return {
+ _id: keepIds ? id : undefined,
+ name: topologyStore[id].name,
+ rooms: rooms,
+ }
+}
+
+export const getAllRooms = function* (roomIds, keepIds) {
+ const roomStore = yield select(OBJECT_SELECTORS['room'])
+
+ let rooms = []
+
+ for (let id of roomIds) {
+ let tiles = yield getAllRoomTiles(roomStore[id], keepIds)
+ rooms.push({
+ _id: keepIds ? id : undefined,
+ name: roomStore[id].name,
+ tiles: tiles,
+ })
+ }
+ return rooms
+}
+
+export const getAllRoomTiles = function* (roomStore, keepIds) {
+ let tiles = []
+
+ for (let id of roomStore.tileIds) {
+ tiles.push(yield getTileById(id, keepIds))
+ }
+ return tiles
+}
+
+export const getTileById = function* (id, keepIds) {
+ const tileStore = yield select(OBJECT_SELECTORS['tile'])
+ return {
+ _id: keepIds ? id : undefined,
+ positionX: tileStore[id].positionX,
+ positionY: tileStore[id].positionY,
+ rack: !tileStore[id].rackId ? undefined : yield getRackById(tileStore[id].rackId, keepIds),
+ }
+}
+
+export const getRackById = function* (id, keepIds) {
+ const rackStore = yield select(OBJECT_SELECTORS['rack'])
+ const machineStore = yield select(OBJECT_SELECTORS['machine'])
+ const cpuStore = yield select(OBJECT_SELECTORS['cpu'])
+ const gpuStore = yield select(OBJECT_SELECTORS['gpu'])
+ const memoryStore = yield select(OBJECT_SELECTORS['memory'])
+ const storageStore = yield select(OBJECT_SELECTORS['storage'])
+
+ return {
+ _id: keepIds ? rackStore[id]._id : undefined,
+ name: rackStore[id].name,
+ capacity: rackStore[id].capacity,
+ powerCapacityW: rackStore[id].powerCapacityW,
+ machines: rackStore[id].machineIds
+ .filter((m) => m !== null)
+ .map((machineId) => ({
+ _id: keepIds ? machineId : undefined,
+ position: machineStore[machineId].position,
+ cpus: machineStore[machineId].cpuIds.map((id) => cpuStore[id]),
+ gpus: machineStore[machineId].gpuIds.map((id) => gpuStore[id]),
+ memories: machineStore[machineId].memoryIds.map((id) => memoryStore[id]),
+ storages: machineStore[machineId].storageIds.map((id) => storageStore[id]),
+ })),
+ }
+}
+
+export const fetchAndStoreAllTraces = () => fetchAndStoreObjects('trace', call(getAllTraces))
+
+export const fetchAndStoreAllSchedulers = function* () {
+ const objects = yield call(getAllSchedulers)
+ for (let object of objects) {
+ object._id = object.name
+ yield put(addToStore('scheduler', object))
+ }
+ return objects
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/portfolios.js b/opendc-web/opendc-web-ui/src/sagas/portfolios.js
new file mode 100644
index 00000000..ed9bfd29
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/portfolios.js
@@ -0,0 +1,131 @@
+import { call, put, select, delay } from 'redux-saga/effects'
+import { addPropToStoreObject, addToStore } from '../actions/objects'
+import { addPortfolio, deletePortfolio, getPortfolio, updatePortfolio } from '../api/routes/portfolios'
+import { getProject } from '../api/routes/projects'
+import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
+import { fetchAndStoreAllTopologiesOfProject } from './topology'
+import { getScenario } from '../api/routes/scenarios'
+
+export function* onOpenPortfolioSucceeded(action) {
+ try {
+ const project = yield call(getProject, action.projectId)
+ yield put(addToStore('project', project))
+ yield fetchAndStoreAllTopologiesOfProject(project._id)
+ yield fetchPortfoliosOfProject()
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+
+ yield watchForPortfolioResults()
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* watchForPortfolioResults() {
+ try {
+ const currentPortfolioId = yield select((state) => state.currentPortfolioId)
+ let unfinishedScenarios = yield getCurrentUnfinishedScenarios()
+
+ while (unfinishedScenarios.length > 0) {
+ yield delay(3000)
+ yield fetchPortfolioWithScenarios(currentPortfolioId)
+ unfinishedScenarios = yield getCurrentUnfinishedScenarios()
+ }
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* getCurrentUnfinishedScenarios() {
+ try {
+ const currentPortfolioId = yield select((state) => state.currentPortfolioId)
+ const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds)
+ const scenarioObjects = yield select((state) => state.objects.scenario)
+ const scenarios = scenarioIds.map((s) => scenarioObjects[s])
+ return scenarios.filter((s) => !s || s.simulation.state === 'QUEUED' || s.simulation.state === 'RUNNING')
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* fetchPortfoliosOfProject() {
+ try {
+ const currentProjectId = yield select((state) => state.currentProjectId)
+ const currentProject = yield select((state) => state.objects.project[currentProjectId])
+
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+
+ for (let i in currentProject.portfolioIds) {
+ yield fetchPortfolioWithScenarios(currentProject.portfolioIds[i])
+ }
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* fetchPortfolioWithScenarios(portfolioId) {
+ try {
+ const portfolio = yield call(getPortfolio, portfolioId)
+ yield put(addToStore('portfolio', portfolio))
+
+ for (let i in portfolio.scenarioIds) {
+ const scenario = yield call(getScenario, portfolio.scenarioIds[i])
+ yield put(addToStore('scenario', scenario))
+ }
+ return portfolio
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddPortfolio(action) {
+ try {
+ const currentProjectId = yield select((state) => state.currentProjectId)
+
+ const portfolio = yield call(
+ addPortfolio,
+ currentProjectId,
+ Object.assign({}, action.portfolio, {
+ projectId: currentProjectId,
+ scenarioIds: [],
+ })
+ )
+ yield put(addToStore('portfolio', portfolio))
+
+ const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds)
+ yield put(
+ addPropToStoreObject('project', currentProjectId, {
+ portfolioIds: portfolioIds.concat([portfolio._id]),
+ })
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onUpdatePortfolio(action) {
+ try {
+ const portfolio = yield call(updatePortfolio, action.portfolio._id, action.portfolio)
+ yield put(addToStore('portfolio', portfolio))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeletePortfolio(action) {
+ try {
+ yield call(deletePortfolio, action.id)
+
+ const currentProjectId = yield select((state) => state.currentProjectId)
+ const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds)
+
+ yield put(
+ addPropToStoreObject('project', currentProjectId, {
+ portfolioIds: portfolioIds.filter((id) => id !== action.id),
+ })
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/prefabs.js b/opendc-web/opendc-web-ui/src/sagas/prefabs.js
new file mode 100644
index 00000000..16cf3d62
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/prefabs.js
@@ -0,0 +1,15 @@
+import { call, put, select } from 'redux-saga/effects'
+import { addToStore } from '../actions/objects'
+import { addPrefab } from '../api/routes/prefabs'
+import { getRackById } from './objects'
+
+export function* onAddPrefab(action) {
+ try {
+ const currentRackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId)
+ const currentRackJson = yield getRackById(currentRackId, false)
+ const prefab = yield call(addPrefab, { name: action.name, rack: currentRackJson })
+ yield put(addToStore('prefab', prefab))
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/profile.js b/opendc-web/opendc-web-ui/src/sagas/profile.js
new file mode 100644
index 00000000..e914ba56
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/profile.js
@@ -0,0 +1,12 @@
+import { call, put } from 'redux-saga/effects'
+import { deleteCurrentUserSucceeded } from '../actions/users'
+import { deleteUser } from '../api/routes/users'
+
+export function* onDeleteCurrentUser(action) {
+ try {
+ yield call(deleteUser, action.userId)
+ yield put(deleteCurrentUserSucceeded())
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/projects.js b/opendc-web/opendc-web-ui/src/sagas/projects.js
new file mode 100644
index 00000000..fdeea132
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/projects.js
@@ -0,0 +1,48 @@
+import { call, put } from 'redux-saga/effects'
+import { addToStore } from '../actions/objects'
+import { addProjectSucceeded, deleteProjectSucceeded } from '../actions/projects'
+import { addProject, deleteProject, getProject } from '../api/routes/projects'
+import { fetchAndStoreAllTopologiesOfProject } from './topology'
+import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
+import { fetchPortfoliosOfProject } from './portfolios'
+
+export function* onOpenProjectSucceeded(action) {
+ try {
+ const project = yield call(getProject, action.id)
+ yield put(addToStore('project', project))
+
+ yield fetchAndStoreAllTopologiesOfProject(action.id, true)
+ yield fetchPortfoliosOfProject()
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onProjectAdd(action) {
+ try {
+ const project = yield call(addProject, { name: action.name })
+ yield put(addToStore('project', project))
+
+ const authorization = {
+ projectId: project._id,
+ userId: action.userId,
+ authorizationLevel: 'OWN',
+ project,
+ }
+ yield put(addToStore('authorization', authorization))
+ yield put(addProjectSucceeded([authorization.userId, authorization.projectId]))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onProjectDelete(action) {
+ try {
+ yield call(deleteProject, action.id)
+ yield put(deleteProjectSucceeded(action.id))
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/scenarios.js b/opendc-web/opendc-web-ui/src/sagas/scenarios.js
new file mode 100644
index 00000000..59223610
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/scenarios.js
@@ -0,0 +1,65 @@
+import { call, put, select } from 'redux-saga/effects'
+import { addPropToStoreObject, addToStore } from '../actions/objects'
+import { getProject } from '../api/routes/projects'
+import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
+import { fetchAndStoreAllTopologiesOfProject } from './topology'
+import { addScenario, deleteScenario, updateScenario } from '../api/routes/scenarios'
+import { fetchPortfolioWithScenarios, watchForPortfolioResults } from './portfolios'
+
+export function* onOpenScenarioSucceeded(action) {
+ try {
+ const project = yield call(getProject, action.projectId)
+ yield put(addToStore('project', project))
+ yield fetchAndStoreAllTopologiesOfProject(project._id)
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+ yield fetchPortfolioWithScenarios(action.portfolioId)
+
+ // TODO Fetch scenario-specific metrics
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddScenario(action) {
+ try {
+ const scenario = yield call(addScenario, action.scenario.portfolioId, action.scenario)
+ yield put(addToStore('scenario', scenario))
+
+ const scenarioIds = yield select((state) => state.objects.portfolio[action.scenario.portfolioId].scenarioIds)
+ yield put(
+ addPropToStoreObject('portfolio', action.scenario.portfolioId, {
+ scenarioIds: scenarioIds.concat([scenario._id]),
+ })
+ )
+ yield watchForPortfolioResults()
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onUpdateScenario(action) {
+ try {
+ const scenario = yield call(updateScenario, action.scenario._id, action.scenario)
+ yield put(addToStore('scenario', scenario))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteScenario(action) {
+ try {
+ yield call(deleteScenario, action.id)
+
+ const currentPortfolioId = yield select((state) => state.currentPortfolioId)
+ const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds)
+
+ yield put(
+ addPropToStoreObject('scenario', currentPortfolioId, {
+ scenarioIds: scenarioIds.filter((id) => id !== action.id),
+ })
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/topology.js b/opendc-web/opendc-web-ui/src/sagas/topology.js
new file mode 100644
index 00000000..bba1ebb1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/topology.js
@@ -0,0 +1,311 @@
+import { call, put, select } from 'redux-saga/effects'
+import { goDownOneInteractionLevel } from '../actions/interaction-level'
+import {
+ addIdToStoreObjectListProp,
+ addPropToStoreObject,
+ addToStore,
+ removeIdFromStoreObjectListProp,
+} from '../actions/objects'
+import {
+ cancelNewRoomConstructionSucceeded,
+ setCurrentTopology,
+ startNewRoomConstructionSucceeded,
+} from '../actions/topology/building'
+import {
+ DEFAULT_RACK_POWER_CAPACITY,
+ DEFAULT_RACK_SLOT_CAPACITY,
+ MAX_NUM_UNITS_PER_MACHINE,
+} from '../components/app/map/MapConstants'
+import { fetchAndStoreTopology, getTopologyAsObject, updateTopologyOnServer } from './objects'
+import { uuid } from 'uuidv4'
+import { addTopology, deleteTopology } from '../api/routes/topologies'
+
+export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = false) {
+ try {
+ const project = yield select((state) => state.objects.project[projectId])
+
+ for (let i in project.topologyIds) {
+ yield fetchAndStoreTopology(project.topologyIds[i])
+ }
+
+ if (setTopology) {
+ yield put(setCurrentTopology(project.topologyIds[0]))
+ }
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddTopology(action) {
+ try {
+ const currentProjectId = yield select((state) => state.currentProjectId)
+
+ let topologyToBeCreated
+ if (action.duplicateId) {
+ topologyToBeCreated = yield getTopologyAsObject(action.duplicateId, false)
+ topologyToBeCreated = Object.assign({}, topologyToBeCreated, {
+ name: action.name,
+ })
+ } else {
+ topologyToBeCreated = { name: action.name, rooms: [] }
+ }
+
+ const topology = yield call(
+ addTopology,
+ Object.assign({}, topologyToBeCreated, {
+ projectId: currentProjectId,
+ })
+ )
+ yield fetchAndStoreTopology(topology._id)
+
+ const topologyIds = yield select((state) => state.objects.project[currentProjectId].topologyIds)
+ yield put(
+ addPropToStoreObject('project', currentProjectId, {
+ topologyIds: topologyIds.concat([topology._id]),
+ })
+ )
+ yield put(setCurrentTopology(topology._id))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteTopology(action) {
+ try {
+ const currentProjectId = yield select((state) => state.currentProjectId)
+ const topologyIds = yield select((state) => state.objects.project[currentProjectId].topologyIds)
+ const currentTopologyId = yield select((state) => state.currentTopologyId)
+ if (currentTopologyId === action.id) {
+ yield put(setCurrentTopology(topologyIds.filter((t) => t !== action.id)[0]))
+ }
+
+ yield call(deleteTopology, action.id)
+
+ yield put(
+ addPropToStoreObject('project', currentProjectId, {
+ topologyIds: topologyIds.filter((id) => id !== action.id),
+ })
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onStartNewRoomConstruction() {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const room = {
+ _id: uuid(),
+ name: 'Room',
+ topologyId,
+ tileIds: [],
+ }
+ yield put(addToStore('room', room))
+ yield put(addIdToStoreObjectListProp('topology', topologyId, 'roomIds', room._id))
+ yield updateTopologyOnServer(topologyId)
+ yield put(startNewRoomConstructionSucceeded(room._id))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onCancelNewRoomConstruction() {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const roomId = yield select((state) => state.construction.currentRoomInConstruction)
+ yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'roomIds', roomId))
+ // TODO remove room from store, too
+ yield updateTopologyOnServer(topologyId)
+ yield put(cancelNewRoomConstructionSucceeded())
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddTile(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const roomId = yield select((state) => state.construction.currentRoomInConstruction)
+ const tile = {
+ _id: uuid(),
+ roomId,
+ positionX: action.positionX,
+ positionY: action.positionY,
+ }
+ yield put(addToStore('tile', tile))
+ yield put(addIdToStoreObjectListProp('room', roomId, 'tileIds', tile._id))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteTile(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const roomId = yield select((state) => state.construction.currentRoomInConstruction)
+ yield put(removeIdFromStoreObjectListProp('room', roomId, 'tileIds', action.tileId))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onEditRoomName(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const roomId = yield select((state) => state.interactionLevel.roomId)
+ const room = Object.assign({}, yield select((state) => state.objects.room[roomId]))
+ room.name = action.name
+ yield put(addPropToStoreObject('room', roomId, { name: action.name }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteRoom() {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const roomId = yield select((state) => state.interactionLevel.roomId)
+ yield put(goDownOneInteractionLevel())
+ yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'roomIds', roomId))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onEditRackName(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId)
+ const rack = Object.assign({}, yield select((state) => state.objects.rack[rackId]))
+ rack.name = action.name
+ yield put(addPropToStoreObject('rack', rackId, { name: action.name }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteRack() {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const tileId = yield select((state) => state.interactionLevel.tileId)
+ yield put(goDownOneInteractionLevel())
+ yield put(addPropToStoreObject('tile', tileId, { rackId: undefined }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddRackToTile(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const rack = {
+ _id: uuid(),
+ name: 'Rack',
+ capacity: DEFAULT_RACK_SLOT_CAPACITY,
+ powerCapacityW: DEFAULT_RACK_POWER_CAPACITY,
+ }
+ rack.machineIds = new Array(rack.capacity).fill(null)
+ yield put(addToStore('rack', rack))
+ yield put(addPropToStoreObject('tile', action.tileId, { rackId: rack._id }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddMachine(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId)
+ const rack = yield select((state) => state.objects.rack[rackId])
+
+ const machine = {
+ _id: uuid(),
+ rackId,
+ position: action.position,
+ cpuIds: [],
+ gpuIds: [],
+ memoryIds: [],
+ storageIds: [],
+ }
+ yield put(addToStore('machine', machine))
+
+ const machineIds = [...rack.machineIds]
+ machineIds[machine.position - 1] = machine._id
+ yield put(addPropToStoreObject('rack', rackId, { machineIds }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteMachine() {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const tileId = yield select((state) => state.interactionLevel.tileId)
+ const position = yield select((state) => state.interactionLevel.position)
+ const rack = yield select((state) => state.objects.rack[state.objects.tile[tileId].rackId])
+ const machineIds = [...rack.machineIds]
+ machineIds[position - 1] = null
+ yield put(goDownOneInteractionLevel())
+ yield put(addPropToStoreObject('rack', rack._id, { machineIds }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddUnit(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const tileId = yield select((state) => state.interactionLevel.tileId)
+ const position = yield select((state) => state.interactionLevel.position)
+ const machine = yield select(
+ (state) =>
+ state.objects.machine[state.objects.rack[state.objects.tile[tileId].rackId].machineIds[position - 1]]
+ )
+
+ if (machine[action.unitType + 'Ids'].length >= MAX_NUM_UNITS_PER_MACHINE) {
+ return
+ }
+
+ const units = [...machine[action.unitType + 'Ids'], action.id]
+ yield put(
+ addPropToStoreObject('machine', machine._id, {
+ [action.unitType + 'Ids']: units,
+ })
+ )
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteUnit(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const tileId = yield select((state) => state.interactionLevel.tileId)
+ const position = yield select((state) => state.interactionLevel.position)
+ const machine = yield select(
+ (state) =>
+ state.objects.machine[state.objects.rack[state.objects.tile[tileId].rackId].machineIds[position - 1]]
+ )
+ const unitIds = machine[action.unitType + 'Ids'].slice()
+ unitIds.splice(action.index, 1)
+
+ yield put(
+ addPropToStoreObject('machine', machine._id, {
+ [action.unitType + 'Ids']: unitIds,
+ })
+ )
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/users.js b/opendc-web/opendc-web-ui/src/sagas/users.js
new file mode 100644
index 00000000..74e652f6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/users.js
@@ -0,0 +1,44 @@
+import { call, put } from 'redux-saga/effects'
+import { logInSucceeded } from '../actions/auth'
+import { addToStore } from '../actions/objects'
+import { fetchAuthorizationsOfCurrentUserSucceeded } from '../actions/users'
+import { performTokenSignIn } from '../api/routes/token-signin'
+import { addUser } from '../api/routes/users'
+import { saveAuthLocalStorage } from '../auth/index'
+import { fetchAndStoreProject, fetchAndStoreUser } from './objects'
+
+export function* onFetchLoggedInUser(action) {
+ try {
+ const tokenResponse = yield call(performTokenSignIn, action.payload.authToken)
+
+ let userId = tokenResponse.userId
+
+ if (tokenResponse.isNewUser) {
+ saveAuthLocalStorage({ authToken: action.payload.authToken })
+ const newUser = yield call(addUser, action.payload)
+ userId = newUser._id
+ }
+
+ yield put(logInSucceeded(Object.assign({ userId }, action.payload)))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onFetchAuthorizationsOfCurrentUser(action) {
+ try {
+ const user = yield call(fetchAndStoreUser, action.userId)
+
+ for (const authorization of user.authorizations) {
+ authorization.userId = action.userId
+ yield put(addToStore('authorization', authorization))
+ yield fetchAndStoreProject(authorization.projectId)
+ }
+
+ const authorizationIds = user.authorizations.map((authorization) => [action.userId, authorization.projectId])
+
+ yield put(fetchAuthorizationsOfCurrentUserSucceeded(authorizationIds))
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/shapes/index.js b/opendc-web/opendc-web-ui/src/shapes/index.js
new file mode 100644
index 00000000..9fab6f5d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/shapes/index.js
@@ -0,0 +1,148 @@
+import PropTypes from 'prop-types'
+
+const Shapes = {}
+
+Shapes.User = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ googleId: PropTypes.string.isRequired,
+ email: PropTypes.string.isRequired,
+ givenName: PropTypes.string.isRequired,
+ familyName: PropTypes.string.isRequired,
+ authorizations: PropTypes.array.isRequired,
+})
+
+Shapes.Project = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ datetimeCreated: PropTypes.string.isRequired,
+ datetimeLastEdited: PropTypes.string.isRequired,
+ topologyIds: PropTypes.array.isRequired,
+ portfolioIds: PropTypes.array.isRequired,
+})
+
+Shapes.Authorization = PropTypes.shape({
+ userId: PropTypes.string.isRequired,
+ user: Shapes.User,
+ projectId: PropTypes.string.isRequired,
+ project: Shapes.Project,
+ authorizationLevel: PropTypes.string.isRequired,
+})
+
+Shapes.ProcessingUnit = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ clockRateMhz: PropTypes.number.isRequired,
+ numberOfCores: PropTypes.number.isRequired,
+ energyConsumptionW: PropTypes.number.isRequired,
+})
+
+Shapes.StorageUnit = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ speedMbPerS: PropTypes.number.isRequired,
+ sizeMb: PropTypes.number.isRequired,
+ energyConsumptionW: PropTypes.number.isRequired,
+})
+
+Shapes.Machine = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ rackId: PropTypes.string.isRequired,
+ position: PropTypes.number.isRequired,
+ cpuIds: PropTypes.arrayOf(PropTypes.string.isRequired),
+ cpus: PropTypes.arrayOf(Shapes.ProcessingUnit),
+ gpuIds: PropTypes.arrayOf(PropTypes.string.isRequired),
+ gpus: PropTypes.arrayOf(Shapes.ProcessingUnit),
+ memoryIds: PropTypes.arrayOf(PropTypes.string.isRequired),
+ memories: PropTypes.arrayOf(Shapes.StorageUnit),
+ storageIds: PropTypes.arrayOf(PropTypes.string.isRequired),
+ storages: PropTypes.arrayOf(Shapes.StorageUnit),
+})
+
+Shapes.Rack = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ capacity: PropTypes.number.isRequired,
+ powerCapacityW: PropTypes.number.isRequired,
+ machines: PropTypes.arrayOf(Shapes.Machine),
+})
+
+Shapes.Tile = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ roomId: PropTypes.string.isRequired,
+ positionX: PropTypes.number.isRequired,
+ positionY: PropTypes.number.isRequired,
+ rackId: PropTypes.string,
+ rack: Shapes.Rack,
+})
+
+Shapes.Room = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ tiles: PropTypes.arrayOf(Shapes.Tile),
+})
+
+Shapes.Topology = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ rooms: PropTypes.arrayOf(Shapes.Room),
+})
+
+Shapes.Scheduler = PropTypes.shape({
+ name: PropTypes.string.isRequired,
+})
+
+Shapes.Trace = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ type: PropTypes.string.isRequired,
+})
+
+Shapes.Portfolio = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ projectId: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ scenarioIds: PropTypes.arrayOf(PropTypes.string).isRequired,
+ targets: PropTypes.shape({
+ enabledMetrics: PropTypes.arrayOf(PropTypes.string).isRequired,
+ repeatsPerScenario: PropTypes.number.isRequired,
+ }).isRequired,
+})
+
+Shapes.Scenario = PropTypes.shape({
+ _id: PropTypes.string.isRequired,
+ portfolioId: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ simulation: PropTypes.shape({
+ state: PropTypes.string.isRequired,
+ }).isRequired,
+ trace: PropTypes.shape({
+ traceId: PropTypes.string.isRequired,
+ trace: Shapes.Trace,
+ loadSamplingFraction: PropTypes.number.isRequired,
+ }).isRequired,
+ topology: PropTypes.shape({
+ topologyId: PropTypes.string.isRequired,
+ topology: Shapes.Topology,
+ }).isRequired,
+ operational: PropTypes.shape({
+ failuresEnabled: PropTypes.bool.isRequired,
+ performanceInterferenceEnabled: PropTypes.bool.isRequired,
+ schedulerName: PropTypes.string.isRequired,
+ scheduler: Shapes.Scheduler,
+ }).isRequired,
+ results: PropTypes.object,
+})
+
+Shapes.WallSegment = PropTypes.shape({
+ startPosX: PropTypes.number.isRequired,
+ startPosY: PropTypes.number.isRequired,
+ isHorizontal: PropTypes.bool.isRequired,
+ length: PropTypes.number.isRequired,
+})
+
+Shapes.InteractionLevel = PropTypes.shape({
+ mode: PropTypes.string.isRequired,
+ roomId: PropTypes.string,
+ rackId: PropTypes.string,
+})
+
+export default Shapes
diff --git a/opendc-web/opendc-web-ui/src/shortcuts/keymap.js b/opendc-web/opendc-web-ui/src/shortcuts/keymap.js
new file mode 100644
index 00000000..797340d7
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/shortcuts/keymap.js
@@ -0,0 +1,10 @@
+const KeymapConfiguration = {
+ MAP: {
+ MOVE_LEFT: ['a', 'left'],
+ MOVE_RIGHT: ['d', 'right'],
+ MOVE_UP: ['w', 'up'],
+ MOVE_DOWN: ['s', 'down'],
+ },
+}
+
+export default KeymapConfiguration
diff --git a/opendc-web/opendc-web-ui/src/store/configure-store.js b/opendc-web/opendc-web-ui/src/store/configure-store.js
new file mode 100644
index 00000000..d8f343ed
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/store/configure-store.js
@@ -0,0 +1,35 @@
+import { applyMiddleware, compose, createStore } from 'redux'
+import persistState from 'redux-localstorage'
+import { createLogger } from 'redux-logger'
+import createSagaMiddleware from 'redux-saga'
+import thunk from 'redux-thunk'
+import { authRedirectMiddleware } from '../auth/index'
+import rootReducer from '../reducers/index'
+import rootSaga from '../sagas/index'
+import { dummyMiddleware } from './middlewares/dummy-middleware'
+import { viewportAdjustmentMiddleware } from './middlewares/viewport-adjustment'
+
+const sagaMiddleware = createSagaMiddleware()
+
+let logger
+if (process.env.NODE_ENV !== 'production') {
+ logger = createLogger()
+}
+
+const middlewares = [
+ process.env.NODE_ENV === 'production' ? dummyMiddleware : logger,
+ thunk,
+ sagaMiddleware,
+ authRedirectMiddleware,
+ viewportAdjustmentMiddleware,
+]
+
+export let store = undefined
+
+export default function configureStore() {
+ const configuredStore = createStore(rootReducer, compose(persistState('auth'), applyMiddleware(...middlewares)))
+ sagaMiddleware.run(rootSaga)
+ store = configuredStore
+
+ return configuredStore
+}
diff --git a/opendc-web/opendc-web-ui/src/store/middlewares/dummy-middleware.js b/opendc-web/opendc-web-ui/src/store/middlewares/dummy-middleware.js
new file mode 100644
index 00000000..5ba35691
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/store/middlewares/dummy-middleware.js
@@ -0,0 +1,3 @@
+export const dummyMiddleware = (store) => (next) => (action) => {
+ next(action)
+}
diff --git a/opendc-web/opendc-web-ui/src/store/middlewares/viewport-adjustment.js b/opendc-web/opendc-web-ui/src/store/middlewares/viewport-adjustment.js
new file mode 100644
index 00000000..b4472c54
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/store/middlewares/viewport-adjustment.js
@@ -0,0 +1,73 @@
+import { SET_MAP_DIMENSIONS, setMapPosition, setMapScale } from '../../actions/map'
+import { SET_CURRENT_TOPOLOGY } from '../../actions/topology/building'
+import {
+ MAP_MAX_SCALE,
+ MAP_MIN_SCALE,
+ SIDEBAR_WIDTH,
+ TILE_SIZE_IN_PIXELS,
+ VIEWPORT_PADDING,
+} from '../../components/app/map/MapConstants'
+import { calculateRoomListBounds } from '../../util/tile-calculations'
+
+export const viewportAdjustmentMiddleware = (store) => (next) => (action) => {
+ const state = store.getState()
+
+ let topologyId = '-1'
+ let mapDimensions = {}
+ if (action.type === SET_CURRENT_TOPOLOGY && action.topologyId !== '-1') {
+ topologyId = action.topologyId
+ mapDimensions = state.map.dimensions
+ } else if (action.type === SET_MAP_DIMENSIONS && state.currentTopologyId !== '-1') {
+ topologyId = state.currentTopologyId
+ mapDimensions = { width: action.width, height: action.height }
+ }
+
+ if (topologyId !== '-1') {
+ const roomIds = state.objects.topology[topologyId].roomIds
+ const rooms = roomIds.map((id) => Object.assign({}, state.objects.room[id]))
+ rooms.forEach((room) => (room.tiles = room.tileIds.map((tileId) => state.objects.tile[tileId])))
+
+ let hasNoTiles = true
+ for (let i in rooms) {
+ if (rooms[i].tiles.length > 0) {
+ hasNoTiles = false
+ break
+ }
+ }
+
+ if (!hasNoTiles) {
+ const viewportParams = calculateParametersToZoomInOnRooms(rooms, mapDimensions.width, mapDimensions.height)
+ store.dispatch(setMapPosition(viewportParams.newX, viewportParams.newY))
+ store.dispatch(setMapScale(viewportParams.newScale))
+ }
+ }
+
+ next(action)
+}
+
+function calculateParametersToZoomInOnRooms(rooms, mapWidth, mapHeight) {
+ const bounds = calculateRoomListBounds(rooms)
+ const newScale = calculateNewScale(bounds, mapWidth, mapHeight)
+
+ // Coordinates of the center of the room, relative to the global origin of the map
+ const roomCenterCoordinates = {
+ x: bounds.center.x * TILE_SIZE_IN_PIXELS * newScale,
+ y: bounds.center.y * TILE_SIZE_IN_PIXELS * newScale,
+ }
+
+ const newX = -roomCenterCoordinates.x + mapWidth / 2
+ const newY = -roomCenterCoordinates.y + mapHeight / 2
+
+ return { newScale, newX, newY }
+}
+
+function calculateNewScale(bounds, mapWidth, mapHeight) {
+ const width = bounds.max.x - bounds.min.x
+ const height = bounds.max.y - bounds.min.y
+
+ const scaleX = (mapWidth - 2 * SIDEBAR_WIDTH) / (width * TILE_SIZE_IN_PIXELS + 2 * VIEWPORT_PADDING)
+ const scaleY = mapHeight / (height * TILE_SIZE_IN_PIXELS + 2 * VIEWPORT_PADDING)
+ const newScale = Math.min(scaleX, scaleY)
+
+ return Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE)
+}
diff --git a/opendc-web/opendc-web-ui/src/style-globals/_mixins.sass b/opendc-web/opendc-web-ui/src/style-globals/_mixins.sass
new file mode 100644
index 00000000..d0a8d1ac
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/style-globals/_mixins.sass
@@ -0,0 +1,21 @@
+=transition($property, $time)
+ -webkit-transition: $property $time
+ -moz-transition: $property $time
+ -o-transition: $property $time
+ transition: $property $time
+
+=user-select
+ -webkit-user-select: none
+ -moz-user-select: none
+ -ms-user-select: none
+ user-select: none
+
+=border-radius($length)
+ -webkit-border-radius: $length
+ -moz-border-radius: $length
+ border-radius: $length
+
+/* General Button Abstractions */
+=clickable
+ cursor: pointer
+ +user-select
diff --git a/opendc-web/opendc-web-ui/src/style-globals/_variables.sass b/opendc-web/opendc-web-ui/src/style-globals/_variables.sass
new file mode 100644
index 00000000..7553caa0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/style-globals/_variables.sass
@@ -0,0 +1,31 @@
+// Sizes and Margins
+$document-padding: 20px
+$inter-element-margin: 5px
+$standard-border-radius: 5px
+$side-menu-width: 350px
+$color-indicator-width: 140px
+
+$global-padding: 30px
+$side-bar-width: 350px
+$navbar-height: 50px
+$navbar-padding: 10px
+
+// Durations
+$transition-length: 150ms
+
+// Colors
+$gray-very-dark: #5c5c5c
+$gray-dark: #aaa
+$gray-semi-dark: #bbb
+$gray-semi-light: #ccc
+$gray-light: #ddd
+$gray-very-light: #eee
+$blue: #00A6D6
+$blue-dark: #0087b5
+$blue-very-dark: #006182
+$blue-light: #deebf7
+
+// Media queries
+$screen-sm: 768px
+$screen-md: 992px
+$screen-lg: 1200px
diff --git a/opendc-web/opendc-web-ui/src/util/authorizations.js b/opendc-web/opendc-web-ui/src/util/authorizations.js
new file mode 100644
index 00000000..4086b35d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/authorizations.js
@@ -0,0 +1,11 @@
+export const AUTH_ICON_MAP = {
+ OWN: 'home',
+ EDIT: 'pencil',
+ VIEW: 'eye',
+}
+
+export const AUTH_DESCRIPTION_MAP = {
+ OWN: 'Own',
+ EDIT: 'Can Edit',
+ VIEW: 'Can View',
+}
diff --git a/opendc-web/opendc-web-ui/src/util/available-metrics.js b/opendc-web/opendc-web-ui/src/util/available-metrics.js
new file mode 100644
index 00000000..807bc0c1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/available-metrics.js
@@ -0,0 +1,67 @@
+export const AVAILABLE_METRICS = [
+ 'total_overcommitted_burst',
+ 'total_power_draw',
+ 'total_failure_vm_slices',
+ 'total_granted_burst',
+ 'total_interfered_burst',
+ 'total_requested_burst',
+ 'mean_cpu_usage',
+ 'mean_cpu_demand',
+ 'mean_num_deployed_images',
+ 'max_num_deployed_images',
+ 'total_vms_submitted',
+ 'total_vms_queued',
+ 'total_vms_finished',
+ 'total_vms_failed',
+]
+
+export const METRIC_NAMES_SHORT = {
+ total_overcommitted_burst: 'Overcomm. CPU Cycles',
+ total_granted_burst: 'Granted CPU Cycles',
+ total_requested_burst: 'Requested CPU Cycles',
+ total_interfered_burst: 'Interfered CPU Cycles',
+ total_power_draw: 'Total Power Consumption',
+ mean_cpu_usage: 'Mean Host CPU Usage',
+ mean_cpu_demand: 'Mean Host CPU Demand',
+ mean_num_deployed_images: 'Mean Num. Deployed Images Per Host',
+ max_num_deployed_images: 'Max. Num. Deployed Images Per Host',
+ total_failure_vm_slices: 'Total Num. Failed VM Slices',
+ total_vms_submitted: 'Total Num. VMs Submitted',
+ total_vms_queued: 'Max. Num. VMs Queued',
+ total_vms_finished: 'Max. Num. VMs Finished',
+ total_vms_failed: 'Max. Num. VMs Failed',
+}
+
+export const METRIC_NAMES = {
+ total_overcommitted_burst: 'Overcommitted CPU Cycles',
+ total_granted_burst: 'Granted CPU Cycles',
+ total_requested_burst: 'Requested CPU Cycles',
+ total_interfered_burst: 'Interfered CPU Cycles',
+ total_power_draw: 'Total Power Consumption',
+ mean_cpu_usage: 'Mean Host CPU Usage',
+ mean_cpu_demand: 'Mean Host CPU Demand',
+ mean_num_deployed_images: 'Mean Number of Deployed Images Per Host',
+ max_num_deployed_images: 'Maximum Number Deployed Images Per Host',
+ total_failure_vm_slices: 'Total Number Failed VM Slices',
+ total_vms_submitted: 'Total Number VMs Submitted',
+ total_vms_queued: 'Maximum Number VMs Queued',
+ total_vms_finished: 'Maximum Number VMs Finished',
+ total_vms_failed: 'Maximum Number VMs Failed',
+}
+
+export const METRIC_UNITS = {
+ total_overcommitted_burst: 'MFLOP',
+ total_granted_burst: 'MFLOP',
+ total_requested_burst: 'MFLOP',
+ total_interfered_burst: 'MFLOP',
+ total_power_draw: 'Wh',
+ mean_cpu_usage: 'MHz',
+ mean_cpu_demand: 'MHz',
+ mean_num_deployed_images: 'VMs',
+ max_num_deployed_images: 'VMs',
+ total_failure_vm_slices: 'VM Slices',
+ total_vms_submitted: 'VMs',
+ total_vms_queued: 'VMs',
+ total_vms_finished: 'VMs',
+ total_vms_failed: 'VMs',
+}
diff --git a/opendc-web/opendc-web-ui/src/util/colors.js b/opendc-web/opendc-web-ui/src/util/colors.js
new file mode 100644
index 00000000..34468503
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/colors.js
@@ -0,0 +1,29 @@
+export const GRID_COLOR = 'rgba(0, 0, 0, 0.5)'
+export const BACKDROP_COLOR = 'rgba(255, 255, 255, 1)'
+export const WALL_COLOR = 'rgba(0, 0, 0, 1)'
+
+export const ROOM_DEFAULT_COLOR = 'rgba(150, 150, 150, 1)'
+export const ROOM_IN_CONSTRUCTION_COLOR = 'rgba(51, 153, 255, 1)'
+export const ROOM_HOVER_VALID_COLOR = 'rgba(51, 153, 255, 1)'
+export const ROOM_HOVER_INVALID_COLOR = 'rgba(255, 102, 0, 1)'
+export const ROOM_NAME_COLOR = 'rgba(245, 245, 245, 1)'
+export const ROOM_TYPE_COLOR = 'rgba(245, 245, 245, 1)'
+
+export const TILE_PLUS_COLOR = 'rgba(0, 0, 0, 1)'
+
+export const OBJECT_BORDER_COLOR = 'rgba(0, 0, 0, 1)'
+
+export const RACK_BACKGROUND_COLOR = 'rgba(170, 170, 170, 1)'
+export const RACK_SPACE_BAR_BACKGROUND_COLOR = 'rgba(222, 235, 247, 0.6)'
+export const RACK_SPACE_BAR_FILL_COLOR = 'rgba(91, 155, 213, 0.7)'
+export const RACK_ENERGY_BAR_BACKGROUND_COLOR = 'rgba(255, 242, 204, 0.6)'
+export const RACK_ENERGY_BAR_FILL_COLOR = 'rgba(244, 215, 0, 0.7)'
+export const COOLING_ITEM_BACKGROUND_COLOR = 'rgba(40, 50, 230, 1)'
+export const PSU_BACKGROUND_COLOR = 'rgba(230, 50, 60, 1)'
+
+export const GRAYED_OUT_AREA_COLOR = 'rgba(0, 0, 0, 0.6)'
+
+export const SIM_LOW_COLOR = 'rgba(197, 224, 180, 1)'
+export const SIM_MID_LOW_COLOR = 'rgba(255, 230, 153, 1)'
+export const SIM_MID_HIGH_COLOR = 'rgba(248, 203, 173, 1)'
+export const SIM_HIGH_COLOR = 'rgba(249, 165, 165, 1)'
diff --git a/opendc-web/opendc-web-ui/src/util/date-time.js b/opendc-web/opendc-web-ui/src/util/date-time.js
new file mode 100644
index 00000000..66efdf5b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/date-time.js
@@ -0,0 +1,93 @@
+/**
+ * Parses and formats the given date-time string representation.
+ *
+ * The format assumed is "YYYY-MM-DDTHH:MM:SS".
+ *
+ * @param dateTimeString A string expressing a date and a time, in the above mentioned format.
+ * @returns {string} A human-friendly string version of that date and time.
+ */
+export function parseAndFormatDateTime(dateTimeString) {
+ return formatDateTime(parseDateTime(dateTimeString))
+}
+
+/**
+ * Parses date-time string representations and returns a parsed object.
+ *
+ * The format assumed is "YYYY-MM-DDTHH:MM:SS".
+ *
+ * @param dateTimeString A string expressing a date and a time, in the above mentioned format.
+ * @returns {object} A Date object with the parsed date and time information as content.
+ */
+export function parseDateTime(dateTimeString) {
+ return new Date(dateTimeString + '.000Z')
+}
+
+/**
+ * Serializes the given date and time value to a human-friendly string.
+ *
+ * @param dateTime An object representation of a date and time.
+ * @returns {string} A human-friendly string version of that date and time.
+ */
+export function formatDateTime(dateTime) {
+ let date
+ const currentDate = new Date()
+
+ date =
+ addPaddingToTwo(dateTime.getDay()) +
+ '/' +
+ addPaddingToTwo(dateTime.getMonth()) +
+ '/' +
+ addPaddingToTwo(dateTime.getFullYear())
+
+ if (dateTime.getFullYear() === currentDate.getFullYear() && dateTime.getMonth() === currentDate.getMonth()) {
+ if (dateTime.getDate() === currentDate.getDate()) {
+ date = 'Today'
+ } else if (dateTime.getDate() === currentDate.getDate() - 1) {
+ date = 'Yesterday'
+ }
+ }
+
+ return date + ', ' + addPaddingToTwo(dateTime.getHours()) + ':' + addPaddingToTwo(dateTime.getMinutes())
+}
+
+/**
+ * Formats the given number of seconds/ticks to a formatted time representation.
+ *
+ * @param seconds The number of seconds.
+ * @returns {string} A string representation of that amount of second, in the from of HH:MM:SS.
+ */
+export function convertSecondsToFormattedTime(seconds) {
+ if (seconds <= 0) {
+ return '0s'
+ }
+
+ let hour = Math.floor(seconds / 3600)
+ let minute = Math.floor(seconds / 60) % 60
+ let second = seconds % 60
+
+ hour = isNaN(hour) ? 0 : hour
+ minute = isNaN(minute) ? 0 : minute
+ second = isNaN(second) ? 0 : second
+
+ if (hour === 0 && minute === 0) {
+ return second + 's'
+ } else if (hour === 0) {
+ return minute + 'm' + addPaddingToTwo(second) + 's'
+ } else {
+ return hour + 'h' + addPaddingToTwo(minute) + 'm' + addPaddingToTwo(second) + 's'
+ }
+}
+
+/**
+ * Pads the given integer to have at least two digits.
+ *
+ * @param integer An integer to be padded.
+ * @returns {string} A string containing the padded integer.
+ */
+function addPaddingToTwo(integer) {
+ if (integer < 10) {
+ return '0' + integer.toString()
+ } else {
+ return integer.toString()
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/util/date-time.test.js b/opendc-web/opendc-web-ui/src/util/date-time.test.js
new file mode 100644
index 00000000..3d95eba6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/date-time.test.js
@@ -0,0 +1,35 @@
+import { convertSecondsToFormattedTime, parseDateTime } from './date-time'
+
+describe('date-time parsing', () => {
+ it('reads components properly', () => {
+ const dateString = '2017-09-27T20:55:01'
+ const parsedDate = parseDateTime(dateString)
+
+ expect(parsedDate.getUTCFullYear()).toEqual(2017)
+ expect(parsedDate.getUTCMonth()).toEqual(8)
+ expect(parsedDate.getUTCDate()).toEqual(27)
+ expect(parsedDate.getUTCHours()).toEqual(20)
+ expect(parsedDate.getUTCMinutes()).toEqual(55)
+ expect(parsedDate.getUTCSeconds()).toEqual(1)
+ })
+})
+
+describe('tick formatting', () => {
+ it("returns '0s' for numbers <= 0", () => {
+ expect(convertSecondsToFormattedTime(-1)).toEqual('0s')
+ expect(convertSecondsToFormattedTime(0)).toEqual('0s')
+ })
+ it('returns only seconds for values under a minute', () => {
+ expect(convertSecondsToFormattedTime(1)).toEqual('1s')
+ expect(convertSecondsToFormattedTime(59)).toEqual('59s')
+ })
+ it('returns seconds and minutes for values under an hour', () => {
+ expect(convertSecondsToFormattedTime(60)).toEqual('1m00s')
+ expect(convertSecondsToFormattedTime(61)).toEqual('1m01s')
+ expect(convertSecondsToFormattedTime(3599)).toEqual('59m59s')
+ })
+ it('returns full time for values over an hour', () => {
+ expect(convertSecondsToFormattedTime(3600)).toEqual('1h00m00s')
+ expect(convertSecondsToFormattedTime(3601)).toEqual('1h00m01s')
+ })
+})
diff --git a/opendc-web/opendc-web-ui/src/util/sidebar-space.js b/opendc-web/opendc-web-ui/src/util/sidebar-space.js
new file mode 100644
index 00000000..ef09d40a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/sidebar-space.js
@@ -0,0 +1,2 @@
+export const isCollapsible = (location) =>
+ location.pathname.indexOf('portfolios') === -1 && location.pathname.indexOf('scenarios') === -1
diff --git a/opendc-web/opendc-web-ui/src/util/state-utils.js b/opendc-web/opendc-web-ui/src/util/state-utils.js
new file mode 100644
index 00000000..e5b695c3
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/state-utils.js
@@ -0,0 +1,6 @@
+export const getState = (dispatch) =>
+ new Promise((resolve) => {
+ dispatch((dispatch, getState) => {
+ resolve(getState())
+ })
+ })
diff --git a/opendc-web/opendc-web-ui/src/util/tile-calculations.js b/opendc-web/opendc-web-ui/src/util/tile-calculations.js
new file mode 100644
index 00000000..764ae6ac
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/tile-calculations.js
@@ -0,0 +1,255 @@
+export function deriveWallLocations(tiles) {
+ const { verticalWalls, horizontalWalls } = getWallSegments(tiles)
+ return mergeWallSegments(verticalWalls, horizontalWalls)
+}
+
+function getWallSegments(tiles) {
+ const verticalWalls = {}
+ const horizontalWalls = {}
+
+ tiles.forEach((tile) => {
+ const x = tile.positionX,
+ y = tile.positionY
+
+ for (let dX = -1; dX <= 1; dX++) {
+ for (let dY = -1; dY <= 1; dY++) {
+ if (Math.abs(dX) === Math.abs(dY)) {
+ continue
+ }
+
+ let doInsert = true
+ for (let tileIndex in tiles) {
+ if (tiles[tileIndex].positionX === x + dX && tiles[tileIndex].positionY === y + dY) {
+ doInsert = false
+ break
+ }
+ }
+ if (!doInsert) {
+ continue
+ }
+
+ if (dX === -1) {
+ if (verticalWalls[x] === undefined) {
+ verticalWalls[x] = []
+ }
+ if (verticalWalls[x].indexOf(y) === -1) {
+ verticalWalls[x].push(y)
+ }
+ } else if (dX === 1) {
+ if (verticalWalls[x + 1] === undefined) {
+ verticalWalls[x + 1] = []
+ }
+ if (verticalWalls[x + 1].indexOf(y) === -1) {
+ verticalWalls[x + 1].push(y)
+ }
+ } else if (dY === -1) {
+ if (horizontalWalls[y] === undefined) {
+ horizontalWalls[y] = []
+ }
+ if (horizontalWalls[y].indexOf(x) === -1) {
+ horizontalWalls[y].push(x)
+ }
+ } else if (dY === 1) {
+ if (horizontalWalls[y + 1] === undefined) {
+ horizontalWalls[y + 1] = []
+ }
+ if (horizontalWalls[y + 1].indexOf(x) === -1) {
+ horizontalWalls[y + 1].push(x)
+ }
+ }
+ }
+ }
+ })
+
+ return { verticalWalls, horizontalWalls }
+}
+
+function mergeWallSegments(vertical, horizontal) {
+ const result = []
+ const walls = [vertical, horizontal]
+
+ for (let i = 0; i < 2; i++) {
+ const wallList = walls[i]
+ for (let a in wallList) {
+ a = parseInt(a, 10)
+
+ wallList[a].sort((a, b) => {
+ return a - b
+ })
+
+ let startPos = wallList[a][0]
+ const isHorizontal = i === 1
+
+ if (wallList[a].length === 1) {
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: 1,
+ })
+ } else {
+ let consecutiveCount = 1
+ for (let b = 0; b < wallList[a].length - 1; b++) {
+ if (b + 1 === wallList[a].length - 1) {
+ if (wallList[a][b + 1] - wallList[a][b] > 1) {
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: consecutiveCount,
+ })
+ consecutiveCount = 0
+ startPos = wallList[a][b + 1]
+ }
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: consecutiveCount + 1,
+ })
+ break
+ } else if (wallList[a][b + 1] - wallList[a][b] > 1) {
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: consecutiveCount,
+ })
+ startPos = wallList[a][b + 1]
+ consecutiveCount = 0
+ }
+ consecutiveCount++
+ }
+ }
+ }
+ }
+
+ return result
+}
+
+export function deriveValidNextTilePositions(rooms, selectedTiles) {
+ const result = [],
+ newPosition = { x: 0, y: 0 }
+ let isSurroundingTile
+
+ selectedTiles.forEach((tile) => {
+ const x = tile.positionX
+ const y = tile.positionY
+ result.push({ x, y })
+
+ for (let dX = -1; dX <= 1; dX++) {
+ for (let dY = -1; dY <= 1; dY++) {
+ if (Math.abs(dX) === Math.abs(dY)) {
+ continue
+ }
+
+ newPosition.x = x + dX
+ newPosition.y = y + dY
+
+ isSurroundingTile = true
+ for (let index in selectedTiles) {
+ if (
+ selectedTiles[index].positionX === newPosition.x &&
+ selectedTiles[index].positionY === newPosition.y
+ ) {
+ isSurroundingTile = false
+ break
+ }
+ }
+
+ if (isSurroundingTile && findPositionInRooms(rooms, newPosition.x, newPosition.y) === -1) {
+ result.push({ x: newPosition.x, y: newPosition.y })
+ }
+ }
+ }
+ })
+
+ return result
+}
+
+export function findPositionInPositions(positions, positionX, positionY) {
+ for (let i = 0; i < positions.length; i++) {
+ const position = positions[i]
+ if (positionX === position.x && positionY === position.y) {
+ return i
+ }
+ }
+
+ return -1
+}
+
+export function findPositionInRooms(rooms, positionX, positionY) {
+ for (let i = 0; i < rooms.length; i++) {
+ const room = rooms[i]
+ if (findPositionInTiles(room.tiles, positionX, positionY) !== -1) {
+ return i
+ }
+ }
+
+ return -1
+}
+
+function findPositionInTiles(tiles, positionX, positionY) {
+ let index = -1
+
+ for (let i = 0; i < tiles.length; i++) {
+ const tile = tiles[i]
+ if (positionX === tile.positionX && positionY === tile.positionY) {
+ index = i
+ break
+ }
+ }
+
+ return index
+}
+
+export function findTileWithPosition(tiles, positionX, positionY) {
+ for (let i = 0; i < tiles.length; i++) {
+ if (tiles[i].positionX === positionX && tiles[i].positionY === positionY) {
+ return tiles[i]
+ }
+ }
+
+ return null
+}
+
+export function calculateRoomListBounds(rooms) {
+ const min = { x: Number.MAX_VALUE, y: Number.MAX_VALUE }
+ const max = { x: -1, y: -1 }
+
+ rooms.forEach((room) => {
+ room.tiles.forEach((tile) => {
+ if (tile.positionX < min.x) {
+ min.x = tile.positionX
+ }
+ if (tile.positionY < min.y) {
+ min.y = tile.positionY
+ }
+
+ if (tile.positionX > max.x) {
+ max.x = tile.positionX
+ }
+ if (tile.positionY > max.y) {
+ max.y = tile.positionY
+ }
+ })
+ })
+
+ max.x++
+ max.y++
+
+ const center = {
+ x: min.x + (max.x - min.x) / 2.0,
+ y: min.y + (max.y - min.y) / 2.0,
+ }
+
+ return { min, center, max }
+}
diff --git a/opendc-web/opendc-web-ui/src/util/timeline.js b/opendc-web/opendc-web-ui/src/util/timeline.js
new file mode 100644
index 00000000..7c8a3ef0
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/timeline.js
@@ -0,0 +1,9 @@
+export function convertTickToPercentage(tick, maxTick) {
+ if (maxTick === 0) {
+ return '0%'
+ } else if (tick > maxTick) {
+ return (maxTick / (maxTick + 1)) * 100 + '%'
+ }
+
+ return (tick / (maxTick + 1)) * 100 + '%'
+}
diff --git a/opendc-web/opendc-web-ui/src/util/unit-specifications.js b/opendc-web/opendc-web-ui/src/util/unit-specifications.js
new file mode 100644
index 00000000..28479edd
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/unit-specifications.js
@@ -0,0 +1,102 @@
+export const CPU_UNITS = {
+ 'cpu-1': {
+ _id: 'cpu-1',
+ name: 'Intel i7 v6 6700k',
+ clockRateMhz: 4100,
+ numberOfCores: 4,
+ energyConsumptionW: 70,
+ },
+ 'cpu-2': {
+ _id: 'cpu-2',
+ name: 'Intel i5 v6 6700k',
+ clockRateMhz: 3500,
+ numberOfCores: 2,
+ energyConsumptionW: 50,
+ },
+ 'cpu-3': {
+ _id: 'cpu-3',
+ name: 'Intel® Xeon® E-2224G',
+ clockRateMhz: 3500,
+ numberOfCores: 4,
+ energyConsumptionW: 71,
+ },
+ 'cpu-4': {
+ _id: 'cpu-4',
+ name: 'Intel® Xeon® E-2244G',
+ clockRateMhz: 3800,
+ numberOfCores: 8,
+ energyConsumptionW: 71,
+ },
+ 'cpu-5': {
+ _id: 'cpu-5',
+ name: 'Intel® Xeon® E-2246G',
+ clockRateMhz: 3600,
+ numberOfCores: 12,
+ energyConsumptionW: 80,
+ },
+}
+
+export const GPU_UNITS = {
+ 'gpu-1': {
+ _id: 'gpu-1',
+ name: 'NVIDIA GTX 4 1080',
+ clockRateMhz: 1200,
+ numberOfCores: 200,
+ energyConsumptionW: 250,
+ },
+ 'gpu-2': {
+ _id: 'gpu-2',
+ name: 'NVIDIA Tesla V100',
+ clockRateMhz: 1200,
+ numberOfCores: 5120,
+ energyConsumptionW: 250,
+ },
+}
+
+export const MEMORY_UNITS = {
+ 'memory-1': {
+ _id: 'memory-1',
+ name: 'Samsung PC DRAM K4A4G045WD',
+ speedMbPerS: 16000,
+ sizeMb: 4000,
+ energyConsumptionW: 10,
+ },
+ 'memory-2': {
+ _id: 'memory-2',
+ name: 'Samsung PC DRAM M393A2K43BB1-CRC',
+ speedMbPerS: 2400,
+ sizeMb: 16000,
+ energyConsumptionW: 10,
+ },
+ 'memory-3': {
+ _id: 'memory-3',
+ name: 'Crucial MTA18ASF4G72PDZ-3G2E1',
+ speedMbPerS: 3200,
+ sizeMb: 32000,
+ energyConsumptionW: 10,
+ },
+ 'memory-4': {
+ _id: 'memory-4',
+ name: 'Crucial MTA9ASF2G72PZ-3G2E1',
+ speedMbPerS: 3200,
+ sizeMb: 16000,
+ energyConsumptionW: 10,
+ },
+}
+
+export const STORAGE_UNITS = {
+ 'storage-1': {
+ _id: 'storage-1',
+ name: 'Samsung EVO 2016 SATA III',
+ speedMbPerS: 6000,
+ sizeMb: 250000,
+ energyConsumptionW: 10,
+ },
+ 'storage-2': {
+ _id: 'storage-2',
+ name: 'Western Digital MTA9ASF2G72PZ-3G2E1',
+ speedMbPerS: 6000,
+ sizeMb: 4000000,
+ energyConsumptionW: 10,
+ },
+}