summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-07-16 10:32:57 +0200
committerGitHub <noreply@github.com>2021-07-16 10:32:57 +0200
commitdb1d2c2f8c18850dedf34b5d690b6cd6a1d1f6b5 (patch)
tree263a6f9741c5ca0dd64ecf3f7f07b580331aec9d
parent1a2416043f0b877f570e89da74e0d0a4aff1d8ae (diff)
parent803e13b32cf0ff8b496649fb0a4d6e32400e98a4 (diff)
merge: Add PatternFly 4 web interface (#161)
This pull requests adds the new web interface based on the PatternFly 4 design framework. This framework enables us to develop more quickly the interfaces necessary in OpenDC. * Remove the OpenDC landing page from the web interface module * Add support for the PatternFly 4 framework in Next.js * Relax topology schema requirements * Migrate UI components to PatternFly 4
-rw-r--r--opendc-web/opendc-web-api/opendc/models/topology.py6
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Machine.kt3
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Room.kt3
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/RoomTile.kt3
-rw-r--r--opendc-web/opendc-web-ui/next.config.js22
-rw-r--r--opendc-web/opendc-web-ui/package.json11
-rw-r--r--opendc-web/opendc-web-ui/public/index.html70
-rw-r--r--opendc-web/opendc-web-ui/src/auth.js8
-rw-r--r--opendc-web/opendc-web-ui/src/components/AppHeader.js44
-rw-r--r--opendc-web/opendc-web-ui/src/components/AppHeaderTools.js133
-rw-r--r--opendc-web/opendc-web-ui/src/components/AppLogo.js46
-rw-r--r--opendc-web/opendc-web-ui/src/components/AppLogo.module.scss33
-rw-r--r--opendc-web/opendc-web-ui/src/components/AppNavigation.js75
-rw-r--r--opendc-web/opendc-web-ui/src/components/AppPage.js41
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/GrayContainer.js34
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js12
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js4
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/MapStage.js66
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/MapStage.module.scss31
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js97
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/RackContainer.js37
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/RackEnergyFillContainer.js (renamed from opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js)5
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js43
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/RoomContainer.js45
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/TileContainer.js46
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/TopologyContainer.js35
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/WallContainer.js39
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.js42
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.module.scss55
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js15
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicator.js (renamed from opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js)8
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicator.module.scss (renamed from opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.module.scss)0
-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.module.scss6
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.js28
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.module.scss29
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js31
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js4
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js6
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js4
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js33
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayer.js (renamed from opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js)22
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayer.js (renamed from opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js)22
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultInfo.js40
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/results/PortfolioResults.js134
-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.js48
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.module.scss57
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js71
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js21
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js45
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js56
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js73
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.js83
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.module.scss37
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js36
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js9
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js41
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionContainer.js46
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js17
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachine.js54
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js39
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarContainer.js37
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js48
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddContainer.js42
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js67
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js122
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListContainer.js56
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js97
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js8
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabContainer.js33
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js18
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackContainer.js54
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js24
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js51
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js69
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.module.scss3
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListContainer.js56
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameContainer.js22
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js61
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.module.scss6
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarContainer.js32
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js17
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js18
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomContainer.js54
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomContainer.js56
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js18
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionContainer.js46
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomName.js44
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js43
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarContainer.js32
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ContactSection.js66
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ContactSection.module.scss20
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ContentSection.js23
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ContentSection.module.scss11
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/IntroSection.js50
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.js34
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.module.scss31
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ModelingSection.js28
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.js39
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.module.scss5
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/SimulationSection.js66
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js47
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/TeamSection.js82
-rw-r--r--opendc-web/opendc-web-ui/src/components/home/TechnologiesSection.js47
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js6
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/Modal.js48
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js65
-rw-r--r--opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModal.js139
-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/NewScenarioModal.js159
-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/NewTopologyModal.js81
-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.js37
-rw-r--r--opendc-web/opendc-web-ui/src/components/navigation/HomeNavbar.js16
-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.js120
-rw-r--r--opendc-web/opendc-web-ui/src/components/navigation/Navbar.module.scss36
-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.module.scss13
-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.module.scss4
-rw-r--r--opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js37
-rw-r--r--opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.module.scss61
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js18
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js53
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewProject.js39
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss26
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewScenario.js64
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/NewTopology.js58
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js97
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js38
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectList.js41
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js29
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js76
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ScenarioState.js62
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js108
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js95
-rw-r--r--opendc-web/opendc-web-ui/src/components/util/BreadcrumbLink.js37
-rw-r--r--opendc-web/opendc-web-ui/src/components/util/NavItemLink.js37
-rw-r--r--opendc-web/opendc-web-ui/src/components/util/TableEmptyState.js103
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js26
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js10
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js16
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js23
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js19
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js13
-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.js10
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js13
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js11
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js14
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js48
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js11
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js67
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js67
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js10
-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.js28
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js11
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js34
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js9
-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.js15
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js34
-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.js11
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js11
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js34
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js29
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js33
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js10
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js34
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js35
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js24
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js31
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js10
-rw-r--r--opendc-web/opendc-web-ui/src/containers/auth/Login.js21
-rw-r--r--opendc-web/opendc-web-ui/src/containers/auth/Logout.js10
-rw-r--r--opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js9
-rw-r--r--opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js11
-rw-r--r--opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js34
-rw-r--r--opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js14
-rw-r--r--opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js34
-rw-r--r--opendc-web/opendc-web-ui/src/data/map.js4
-rw-r--r--opendc-web/opendc-web-ui/src/data/project.js52
-rw-r--r--opendc-web/opendc-web-ui/src/data/topology.js10
-rw-r--r--opendc-web/opendc-web-ui/src/index.scss68
-rw-r--r--opendc-web/opendc-web-ui/src/pages/404.js33
-rw-r--r--opendc-web/opendc-web-ui/src/pages/404.module.scss8
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_app.js14
-rw-r--r--opendc-web/opendc-web-ui/src/pages/index.js42
-rw-r--r--opendc-web/opendc-web-ui/src/pages/index.module.scss16
-rw-r--r--opendc-web/opendc-web-ui/src/pages/logout.js10
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js111
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js170
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js73
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/index.js90
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/topology.js6
-rw-r--r--opendc-web/opendc-web-ui/src/shapes.js2
-rw-r--r--opendc-web/opendc-web-ui/src/style/_mixins.scss5
-rw-r--r--opendc-web/opendc-web-ui/src/style/_variables.scss31
-rw-r--r--opendc-web/opendc-web-ui/src/style/index.scss36
-rw-r--r--opendc-web/opendc-web-ui/src/util/authorizations.js10
-rw-r--r--opendc-web/opendc-web-ui/src/util/available-metrics.js45
-rw-r--r--opendc-web/opendc-web-ui/src/util/date-time.js14
-rw-r--r--opendc-web/opendc-web-ui/src/util/date-time.test.js16
-rw-r--r--opendc-web/opendc-web-ui/yarn.lock211
213 files changed, 4430 insertions, 3836 deletions
diff --git a/opendc-web/opendc-web-api/opendc/models/topology.py b/opendc-web/opendc-web-api/opendc/models/topology.py
index 592f82c5..44994818 100644
--- a/opendc-web/opendc-web-api/opendc/models/topology.py
+++ b/opendc-web/opendc-web-api/opendc/models/topology.py
@@ -38,6 +38,7 @@ class MachineSchema(Schema):
gpus = fields.List(fields.Nested(PuSchema))
memories = fields.List(fields.Nested(MemorySchema))
storages = fields.List(fields.Nested(MemorySchema))
+ rackId = fields.String()
class ObjectSchema(Schema):
@@ -49,6 +50,7 @@ class ObjectSchema(Schema):
capacity = fields.Integer()
powerCapacityW = fields.Integer()
machines = fields.List(fields.Nested(MachineSchema))
+ tileId = fields.String()
class TileSchema(Schema):
@@ -56,9 +58,11 @@ class TileSchema(Schema):
Schema representing a room tile.
"""
_id = fields.String()
+ topologyId = fields.String()
positionX = fields.Integer()
positionY = fields.Integer()
rack = fields.Nested(ObjectSchema)
+ roomId = fields.String()
class RoomSchema(Schema):
@@ -67,6 +71,7 @@ class RoomSchema(Schema):
"""
_id = fields.String()
name = fields.String(required=True)
+ topologyId = fields.String()
tiles = fields.List(fields.Nested(TileSchema), required=True)
@@ -78,6 +83,7 @@ class TopologySchema(Schema):
projectId = fields.String()
name = fields.String(required=True)
rooms = fields.List(fields.Nested(RoomSchema), required=True)
+ datetimeLastEdited = fields.DateTime()
class Topology(Model):
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Machine.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Machine.kt
index c6757c5c..86d2d46f 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Machine.kt
+++ b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Machine.kt
@@ -38,5 +38,6 @@ public data class Machine(
@JsonProperty("memories")
val memory: List<MemoryUnit> = emptyList(),
@JsonProperty("storages")
- val storage: List<MemoryUnit> = emptyList()
+ val storage: List<MemoryUnit> = emptyList(),
+ val rackId: String? = null
)
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Room.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Room.kt
index e961d6db..f1b8f946 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Room.kt
+++ b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/Room.kt
@@ -33,5 +33,6 @@ public data class Room(
@JsonProperty("_id")
val id: String,
val name: String,
- val tiles: Set<RoomTile>
+ val tiles: Set<RoomTile>,
+ val topologyId: String? = null,
)
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/RoomTile.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/RoomTile.kt
index 3bee3204..0b956262 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/RoomTile.kt
+++ b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/client/model/RoomTile.kt
@@ -34,5 +34,6 @@ public data class RoomTile(
val id: String,
val positionX: Double,
val positionY: Double,
- val rack: Rack? = null
+ val rack: Rack? = null,
+ val roomId: String? = null,
)
diff --git a/opendc-web/opendc-web-ui/next.config.js b/opendc-web/opendc-web-ui/next.config.js
index 44ebfff6..9092adc4 100644
--- a/opendc-web/opendc-web-ui/next.config.js
+++ b/opendc-web/opendc-web-ui/next.config.js
@@ -20,9 +20,27 @@
* SOFTWARE.
*/
-module.exports = {
+// PatternFly 4 uses global CSS imports in its distribution files. Therefore,
+// we need to transpile the modules before we can use them.
+const withTM = require('next-transpile-modules')([
+ '@patternfly/react-core',
+ '@patternfly/react-styles',
+ '@patternfly/react-table',
+ '@patternfly/react-tokens',
+])
+
+module.exports = withTM({
reactStrictMode: true,
experimental: {
eslint: true,
},
-}
+ async redirects() {
+ return [
+ {
+ source: '/',
+ destination: '/projects',
+ permanent: true,
+ },
+ ]
+ },
+})
diff --git a/opendc-web/opendc-web-ui/package.json b/opendc-web/opendc-web-ui/package.json
index 1a906acd..84db4277 100644
--- a/opendc-web/opendc-web-ui/package.json
+++ b/opendc-web/opendc-web-ui/package.json
@@ -10,10 +10,10 @@
],
"homepage": "http://opendc.org",
"bugs": {
- "url": "https://github.com/atlarge-research/opendc-frontend/issues",
+ "url": "https://github.com/atlarge-research/opendc/issues",
"email": "opendc@atlarge-research.com"
},
- "author": "Georgios Andreadis <g.andreadis@atlarge-research.com> (https://gandreadis.com/)",
+ "author": "OpenDC Maintainers <opendc@atlarge-research.com>",
"license": "MIT",
"private": true,
"dependencies": {
@@ -22,16 +22,19 @@
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14",
+ "@patternfly/react-core": "^4.135.7",
+ "@patternfly/react-icons": "^4.11.2",
+ "@patternfly/react-table": "^4.29.8",
"@sentry/react": "^5.30.0",
"@sentry/tracing": "^5.30.0",
"approximate-number": "~2.0.0",
- "bootstrap": "~4.6.0",
"classnames": "~2.2.5",
"husky": "~4.2.5",
"konva": "~7.2.5",
"lint-staged": "~10.2.2",
"mathjs": "~7.6.0",
"next": "^11.0.1",
+ "next-transpile-modules": "^8.0.0",
"normalizr": "^3.6.1",
"prettier": "~2.0.5",
"prop-types": "~15.7.2",
@@ -41,7 +44,6 @@
"react-konva": "~17.0.2-0",
"react-query": "^3.18.1",
"react-redux": "~7.2.0",
- "reactstrap": "^8.9.0",
"recharts": "~2.0.9",
"redux": "~4.0.5",
"redux-logger": "~3.0.6",
@@ -49,6 +51,7 @@
"redux-thunk": "~2.3.0",
"sass": "^1.32.12",
"svgsaver": "~0.9.0",
+ "use-resize-observer": "^7.0.0",
"uuidv4": "~6.1.1"
},
"devDependencies": {
diff --git a/opendc-web/opendc-web-ui/public/index.html b/opendc-web/opendc-web-ui/public/index.html
deleted file mode 100644
index 19b80e29..00000000
--- a/opendc-web/opendc-web-ui/public/index.html
+++ /dev/null
@@ -1,70 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
- <meta charset="utf-8">
- <title>OpenDC</title>
-
- <!-- Standard meta tags -->
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
- <meta name="theme-color" content="#00A6D6">
- <meta name="description" content="Collaborative Datacenter Simulation and Exploration for Everybody">
- <meta name="author" content="@Large Research">
- <meta name="keywords" content="OpenDC, Datacenter, Simulation, Simulator, Collaborative, Distributed, Cluster">
- <link rel="manifest" href="/manifest.json">
- <link rel="shortcut icon" href="/favicon.ico">
-
- <!-- Twitter Card data -->
- <meta name="twitter:card" content="summary">
- <meta name="twitter:site" content="@LargeResearch">
- <meta name="twitter:title" content="OpenDC">
- <meta name="twitter:description" content="Collaborative Datacenter Simulation and Exploration for Everybody">
- <meta name="twitter:creator" content="@LargeResearch">
- <meta name="twitter:image" content="http://opendc.org/img/logo.png">
-
- <!-- OpenGraph meta tags -->
- <meta property="og:title" content="OpenDC">
- <meta property="og:site_name" content="OpenDC">
- <meta property="og:type" content="website">
- <meta property="og:image" content="http://opendc.org/img/logo.png">
- <meta property="og:url" content="http://opendc.org/">
- <meta property="og:description"
- content="OpenDC provides collaborative online datacenter modeling, diverse and effective datacenter
- simulation, and exploratory datacenter performance feedback.">
- <meta property="og:locale" content="en_US">
-
- <!-- Google meta tags -->
- <meta name="google-signin-client_id" content="%REACT_APP_OAUTH_CLIENT_ID%">
- <meta name="google-site-verification" content="YIR4LkQTv6WmOdWv8MkeiUKni-0Yu3WHylLp4VvUMig"/>
-
- <!-- CDN dependencies -->
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
- <script src="https://use.fontawesome.com/ece66a2e7c.js"></script>
-
- <!-- Google Analytics -->
- <script async src="https://www.googletagmanager.com/gtag/js?id=UA-84285092-3"></script>
- <script>
- window.dataLayer = window.dataLayer || []
-
- function gtag() {
- dataLayer.push(arguments)
- }
-
- gtag('js', new Date())
- gtag('config', 'UA-84285092-3')
- </script>
- <script>
- /* Dynamic app variables */
- window.config_overrides = {
- REACT_APP_API_BASE_URL: "$REACT_APP_API_BASE_URL",
- REACT_APP_OAUTH_CLIENT_ID: "$REACT_APP_OAUTH_CLIENT_ID",
- REACT_APP_SENTRY_DSN: "$REACT_APP_SENTRY_DSN",
- };
- </script>
-</head>
-<body>
-<noscript>
- You need to enable JavaScript to run this app.
-</noscript>
-<div id="root"></div>
-</body>
-</html>
diff --git a/opendc-web/opendc-web-ui/src/auth.js b/opendc-web/opendc-web-ui/src/auth.js
index 96417e67..e670476c 100644
--- a/opendc-web/opendc-web-ui/src/auth.js
+++ b/opendc-web/opendc-web-ui/src/auth.js
@@ -23,7 +23,6 @@
import PropTypes from 'prop-types'
import { Auth0Provider, useAuth0 } from '@auth0/auth0-react'
import { useEffect } from 'react'
-import { useRouter } from 'next/router'
/**
* Obtain the authentication context.
@@ -37,14 +36,13 @@ export function useAuth() {
*/
export function useRequireAuth() {
const auth = useAuth()
- const router = useRouter()
- const { isLoading, isAuthenticated } = auth
+ const { loginWithRedirect, isLoading, isAuthenticated } = auth
useEffect(() => {
if (!isLoading && !isAuthenticated) {
- router.replace('/')
+ loginWithRedirect()
}
- }, [router, isLoading, isAuthenticated])
+ }, [loginWithRedirect, isLoading, isAuthenticated])
return auth
}
diff --git a/opendc-web/opendc-web-ui/src/components/AppHeader.js b/opendc-web/opendc-web-ui/src/components/AppHeader.js
new file mode 100644
index 00000000..b33212c4
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/AppHeader.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { PageHeader } from '@patternfly/react-core'
+import React from 'react'
+import Image from 'next/image'
+import AppHeaderTools from './AppHeaderTools'
+import { AppNavigation } from './AppNavigation'
+import AppLogo from './AppLogo'
+
+export function AppHeader() {
+ const logo = <Image src="/img/logo.png" layout="fixed" width={30} height={30} alt="OpenDC" />
+
+ return (
+ <PageHeader
+ logo={logo}
+ logoProps={{ href: '/' }}
+ logoComponent={AppLogo}
+ headerTools={<AppHeaderTools />}
+ topNav={<AppNavigation />}
+ />
+ )
+}
+
+AppHeader.propTypes = {}
diff --git a/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js
new file mode 100644
index 00000000..02e5d265
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import {
+ Avatar,
+ Button,
+ ButtonVariant,
+ Dropdown,
+ DropdownGroup,
+ DropdownItem,
+ DropdownToggle,
+ KebabToggle,
+ PageHeaderTools,
+ PageHeaderToolsGroup,
+ PageHeaderToolsItem,
+ Skeleton,
+} from '@patternfly/react-core'
+import { useState } from 'react'
+import { useAuth } from '../auth'
+import { GithubIcon, HelpIcon } from '@patternfly/react-icons'
+
+function AppHeaderTools() {
+ const auth = useAuth()
+
+ const [isKebabDropdownOpen, setKebabDropdownOpen] = useState(false)
+ const kebabDropdownItems = [
+ <DropdownItem
+ key={0}
+ component={
+ <a href="https://opendc.org" target="_blank" rel="noreferrer">
+ <HelpIcon /> Help
+ </a>
+ }
+ />,
+ ]
+
+ const [isDropdownOpen, setDropdownOpen] = useState(false)
+ const userDropdownItems = [
+ <DropdownGroup key="group 2">
+ <DropdownItem key="group 2 logout" onClick={() => auth.logout({ returnTo: window.location.origin })}>
+ Logout
+ </DropdownItem>
+ </DropdownGroup>,
+ ]
+
+ return (
+ <PageHeaderTools>
+ <PageHeaderToolsGroup visibility={{ default: 'hidden', lg: 'visible' }}>
+ <PageHeaderToolsItem>
+ <Button
+ component="a"
+ href="https://github.com/atlarge-research/opendc"
+ target="_blank"
+ aria-label="Source code"
+ variant={ButtonVariant.plain}
+ >
+ <GithubIcon />
+ </Button>
+ </PageHeaderToolsItem>
+ <PageHeaderToolsItem>
+ <Button
+ component="a"
+ href="https://opendc.org/"
+ target="_blank"
+ aria-label="Help actions"
+ variant={ButtonVariant.plain}
+ >
+ <HelpIcon />
+ </Button>
+ </PageHeaderToolsItem>
+ </PageHeaderToolsGroup>
+ <PageHeaderToolsGroup>
+ <PageHeaderToolsItem visibility={{ lg: 'hidden' }}>
+ <Dropdown
+ isPlain
+ position="right"
+ toggle={<KebabToggle onToggle={() => setKebabDropdownOpen(!isKebabDropdownOpen)} />}
+ isOpen={isKebabDropdownOpen}
+ dropdownItems={kebabDropdownItems}
+ />
+ </PageHeaderToolsItem>
+ <PageHeaderToolsItem visibility={{ default: 'hidden', md: 'visible' }}>
+ <Dropdown
+ isPlain
+ position="right"
+ isOpen={isDropdownOpen}
+ toggle={
+ <DropdownToggle onToggle={() => setDropdownOpen(!isDropdownOpen)}>
+ {auth?.user?.name ?? (
+ <Skeleton
+ fontSize="xs"
+ width="150px"
+ className="pf-u-display-inline-flex"
+ screenreaderText="Loading username"
+ />
+ )}
+ </DropdownToggle>
+ }
+ dropdownItems={userDropdownItems}
+ />
+ </PageHeaderToolsItem>
+ </PageHeaderToolsGroup>
+ {auth?.user?.picture ? (
+ <Avatar src={auth.user.picture} alt="Avatar image" />
+ ) : (
+ <Skeleton className="pf-c-avatar" shape="circle" width="2.25rem" screenreaderText="Loading avatar" />
+ )}
+ </PageHeaderTools>
+ )
+}
+
+AppHeaderTools.propTypes = {}
+
+export default AppHeaderTools
diff --git a/opendc-web/opendc-web-ui/src/components/AppLogo.js b/opendc-web/opendc-web-ui/src/components/AppLogo.js
new file mode 100644
index 00000000..92663295
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/AppLogo.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import Link from 'next/link'
+import { appLogo } from './AppLogo.module.scss'
+
+function AppLogo({ href, children, className, ...props }) {
+ return (
+ <>
+ <Link href={href}>
+ <a {...props} className={`${className ?? ''} ${appLogo}`}>
+ {children}
+ <span>OpenDC</span>
+ </a>
+ </Link>
+ </>
+ )
+}
+
+AppLogo.propTypes = {
+ href: PropTypes.string.isRequired,
+ children: PropTypes.node,
+ className: PropTypes.string,
+}
+
+export default AppLogo
diff --git a/opendc-web/opendc-web-ui/src/components/AppLogo.module.scss b/opendc-web/opendc-web-ui/src/components/AppLogo.module.scss
new file mode 100644
index 00000000..3d228cb6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/AppLogo.module.scss
@@ -0,0 +1,33 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.appLogo {
+ span {
+ margin-left: 4px;
+ color: #fff;
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/components/AppNavigation.js b/opendc-web/opendc-web-ui/src/components/AppNavigation.js
new file mode 100644
index 00000000..b3f11f34
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/AppNavigation.js
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { Dropdown, DropdownItem, DropdownToggle, Nav, NavItem, NavList } from '@patternfly/react-core'
+import { useRouter } from 'next/router'
+import NavItemLink from './util/NavItemLink'
+import { useProject } from '../data/project'
+
+export function AppNavigation() {
+ const { pathname, query } = useRouter()
+ const { project: projectId } = query
+ const { data: project } = useProject(projectId)
+
+ const nextTopologyId = project?.topologyIds?.[0]
+ const nextPortfolioId = project?.portfolioIds?.[0]
+
+ return (
+ <Nav variant="horizontal">
+ <NavList>
+ <NavItem
+ id="projects"
+ to="/projects"
+ itemId={0}
+ component={NavItemLink}
+ isActive={pathname === '/projects' || pathname === '/projects/[project]'}
+ >
+ Projects
+ </NavItem>
+ {pathname.startsWith('/projects/[project]') && (
+ <>
+ <NavItem
+ id="topologies"
+ to={nextTopologyId ? `/projects/${projectId}/topologies/${nextTopologyId}` : '/projects'}
+ itemId={1}
+ component={NavItemLink}
+ isActive={pathname === '/projects/[project]/topologies/[topology]'}
+ >
+ Topologies
+ </NavItem>
+ <NavItem
+ id="portfolios"
+ to={nextPortfolioId ? `/projects/${projectId}/portfolios/${nextPortfolioId}` : '/projects'}
+ itemId={2}
+ component={NavItemLink}
+ isActive={pathname === '/projects/[project]/portfolios/[portfolio]'}
+ >
+ Portfolios
+ </NavItem>
+ </>
+ )}
+ </NavList>
+ </Nav>
+ )
+}
+
+AppNavigation.propTypes = {}
diff --git a/opendc-web/opendc-web-ui/src/components/AppPage.js b/opendc-web/opendc-web-ui/src/components/AppPage.js
new file mode 100644
index 00000000..7cf9cc15
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/AppPage.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { AppHeader } from './AppHeader'
+import { AppNavigation } from './AppNavigation'
+import React, { useState } from 'react'
+import { Page } from '@patternfly/react-core'
+
+export function AppPage({ children, breadcrumb, tertiaryNav }) {
+ return (
+ <Page breadcrumb={breadcrumb} tertiaryNav={tertiaryNav} header={<AppHeader />}>
+ {children}
+ </Page>
+ )
+}
+
+AppPage.propTypes = {
+ breadcrumb: PropTypes.node,
+ tertiaryNav: PropTypes.node,
+ children: PropTypes.node,
+}
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/GrayContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/GrayContainer.js
new file mode 100644
index 00000000..4791940f
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/GrayContainer.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useDispatch } from 'react-redux'
+import { goDownOneInteractionLevel } from '../../../redux/actions/interaction-level'
+import GrayLayer from '../../../components/app/map/elements/GrayLayer'
+
+const GrayContainer = () => {
+ const dispatch = useDispatch()
+ const onClick = () => dispatch(goDownOneInteractionLevel())
+ return <GrayLayer onClick={onClick} />
+}
+
+export default GrayContainer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js b/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js
deleted file mode 100644
index ddb94990..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faSpinner } from '@fortawesome/free-solid-svg-icons'
-
-const LoadingScreen = () => (
- <div className="display-4">
- <FontAwesomeIcon icon={faSpinner} spin className="mr-4" />
- 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
index d6ea1f84..45799f70 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js
@@ -8,8 +8,8 @@ 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 WALL_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 16
+export const OBJECT_BORDER_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 16
export const TILE_PLUS_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 10
export const SIDEBAR_WIDTH = 350
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapStage.js b/opendc-web/opendc-web-ui/src/components/app/map/MapStage.js
new file mode 100644
index 00000000..73accf3f
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/MapStage.js
@@ -0,0 +1,66 @@
+import React, { useEffect, useRef, useState } from 'react'
+import { HotKeys } from 'react-hotkeys'
+import { Stage } from 'react-konva'
+import { MAP_MOVE_PIXELS_PER_EVENT } from './MapConstants'
+import { Provider, useDispatch, useStore } from 'react-redux'
+import useResizeObserver from 'use-resize-observer'
+import { mapContainer } from './MapStage.module.scss'
+import { useMapPosition } from '../../../data/map'
+import { setMapDimensions, setMapPositionWithBoundsCheck, zoomInOnPosition } from '../../../redux/actions/map'
+import MapLayer from './layers/MapLayer'
+import RoomHoverLayer from './layers/RoomHoverLayer'
+import ObjectHoverLayer from './layers/ObjectHoverLayer'
+
+function MapStage() {
+ const store = useStore()
+ const dispatch = useDispatch()
+
+ const stage = useRef(null)
+ const [pos, setPos] = useState([0, 0])
+ const [x, y] = pos
+ const handlers = {
+ MOVE_LEFT: () => moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0),
+ MOVE_RIGHT: () => moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0),
+ MOVE_UP: () => moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT),
+ MOVE_DOWN: () => moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT),
+ }
+ const mapPosition = useMapPosition()
+ const { ref, width = 100, height = 100 } = useResizeObserver()
+
+ const moveWithDelta = (deltaX, deltaY) =>
+ dispatch(setMapPositionWithBoundsCheck(mapPosition.x + deltaX, mapPosition.y + deltaY))
+ const updateMousePosition = () => {
+ if (!stage.current) {
+ return
+ }
+
+ const mousePos = stage.current.getStage().getPointerPosition()
+ setPos([mousePos.x, mousePos.y])
+ }
+ const updateScale = ({ evt }) => dispatch(zoomInOnPosition(evt.deltaY < 0, x, y))
+
+ useEffect(() => {
+ window['exportCanvasToImage'] = () => {
+ const download = document.createElement('a')
+ download.href = stage.current.getStage().toDataURL()
+ download.download = 'opendc-canvas-export-' + Date.now() + '.png'
+ download.click()
+ }
+ }, [stage])
+
+ useEffect(() => dispatch(setMapDimensions(width, height)), [width, height]) // eslint-disable-line react-hooks/exhaustive-deps
+
+ return (
+ <HotKeys handlers={handlers} allowChanges={true} innerRef={ref} className={mapContainer}>
+ <Stage ref={stage} width={width} height={height} onMouseMove={updateMousePosition} onWheel={updateScale}>
+ <Provider store={store}>
+ <MapLayer />
+ <RoomHoverLayer mouseX={x} mouseY={y} />
+ <ObjectHoverLayer mouseX={x} mouseY={y} />
+ </Provider>
+ </Stage>
+ </HotKeys>
+ )
+}
+
+export default MapStage
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapStage.module.scss b/opendc-web/opendc-web-ui/src/components/app/map/MapStage.module.scss
new file mode 100644
index 00000000..d879b4c8
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/MapStage.module.scss
@@ -0,0 +1,31 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.mapContainer {
+ background-color: var(--pf-global--Color--light-200);
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+}
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js
deleted file mode 100644
index c3177fe1..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import PropTypes from 'prop-types'
-import React, { useEffect, useRef, useState } from 'react'
-import { HotKeys } from 'react-hotkeys'
-import { Stage } from 'react-konva'
-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, useStore } from 'react-redux'
-
-function MapStageComponent({
- mapDimensions,
- mapPosition,
- setMapDimensions,
- setMapPositionWithBoundsCheck,
- zoomInOnPosition,
-}) {
- const [pos, setPos] = useState([0, 0])
- const stage = useRef(null)
- const [x, y] = pos
- const handlers = {
- MOVE_LEFT: () => moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0),
- MOVE_RIGHT: () => moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0),
- MOVE_UP: () => moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT),
- MOVE_DOWN: () => moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT),
- }
-
- const moveWithDelta = (deltaX, deltaY) =>
- setMapPositionWithBoundsCheck(mapPosition.x + deltaX, mapPosition.y + deltaY)
- const updateMousePosition = () => {
- if (!stage.current) {
- return
- }
-
- const mousePos = stage.current.getStage().getPointerPosition()
- setPos([mousePos.x, mousePos.y])
- }
-
- const updateDimensions = () => setMapDimensions(window.innerWidth, window.innerHeight - NAVBAR_HEIGHT)
- const updateScale = (e) => zoomInOnPosition(e.deltaY < 0, x, y)
-
- // We explicitly do not specify any dependencies to prevent infinitely dispatching updateDimensions commands
- useEffect(() => {
- updateDimensions()
-
- window.addEventListener('resize', updateDimensions)
- window.addEventListener('wheel', updateScale)
-
- window['exportCanvasToImage'] = () => {
- const download = document.createElement('a')
- download.href = stage.current.getStage().toDataURL()
- download.download = 'opendc-canvas-export-' + Date.now() + '.png'
- download.click()
- }
-
- return () => {
- window.removeEventListener('resize', updateDimensions)
- window.removeEventListener('wheel', updateScale)
- }
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
-
- const store = useStore()
-
- return (
- <HotKeys handlers={handlers} allowChanges={true}>
- <Stage
- ref={stage}
- width={mapDimensions.width}
- height={mapDimensions.height}
- onMouseMove={updateMousePosition}
- >
- <Provider store={store}>
- <MapLayer />
- <RoomHoverLayer mouseX={x} mouseY={y} />
- <ObjectHoverLayer mouseX={x} mouseY={y} />
- </Provider>
- </Stage>
- </HotKeys>
- )
-}
-
-MapStageComponent.propTypes = {
- mapDimensions: PropTypes.shape({
- width: PropTypes.number.isRequired,
- height: PropTypes.number.isRequired,
- }).isRequired,
- mapPosition: PropTypes.shape({
- x: PropTypes.number.isRequired,
- y: PropTypes.number.isRequired,
- }).isRequired,
- setMapDimensions: PropTypes.func,
- setMapPositionWithBoundsCheck: PropTypes.func,
- zoomInOnPosition: PropTypes.func,
-}
-
-export default MapStageComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/RackContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/RackContainer.js
new file mode 100644
index 00000000..3c75d3a7
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/RackContainer.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useSelector } from 'react-redux'
+import RackGroup from '../../../components/app/map/groups/RackGroup'
+import { Tile } from '../../../shapes'
+
+const RackContainer = ({ tile }) => {
+ const interactionLevel = useSelector((state) => state.interactionLevel)
+ return <RackGroup interactionLevel={interactionLevel} tile={tile} />
+}
+
+RackContainer.propTypes = {
+ tile: Tile,
+}
+
+export default RackContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/RackEnergyFillContainer.js
index d22317a5..838aea5a 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/RackEnergyFillContainer.js
@@ -1,4 +1,5 @@
import React from 'react'
+import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import RackFillBar from '../../../components/app/map/elements/RackFillBar'
@@ -29,4 +30,8 @@ const RackSpaceFillContainer = (props) => {
return <RackFillBar {...props} {...state} />
}
+RackSpaceFillContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
export default RackSpaceFillContainer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js
new file mode 100644
index 00000000..6791120e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/RackSpaceFillContainer.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+import { useSelector } from 'react-redux'
+import RackFillBar from '../../../components/app/map/elements/RackFillBar'
+
+const RackSpaceFillContainer = (props) => {
+ const state = useSelector((state) => {
+ const machineIds = state.objects.rack[state.objects.tile[props.tileId].rack].machines
+ return {
+ type: 'space',
+ fillFraction: machineIds.filter((id) => id !== null).length / machineIds.length,
+ }
+ })
+ return <RackFillBar {...props} {...state} />
+}
+
+RackSpaceFillContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default RackSpaceFillContainer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/RoomContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/RoomContainer.js
new file mode 100644
index 00000000..26fbcd7a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/RoomContainer.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { goFromBuildingToRoom } from '../../../redux/actions/interaction-level'
+import RoomGroup from '../../../components/app/map/groups/RoomGroup'
+
+const RoomContainer = (props) => {
+ const state = useSelector((state) => {
+ return {
+ interactionLevel: state.interactionLevel,
+ currentRoomInConstruction: state.construction.currentRoomInConstruction,
+ room: state.objects.room[props.roomId],
+ }
+ })
+ const dispatch = useDispatch()
+ return <RoomGroup {...props} {...state} onClick={() => dispatch(goFromBuildingToRoom(props.roomId))} />
+}
+
+RoomContainer.propTypes = {
+ roomId: PropTypes.string,
+}
+
+export default RoomContainer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/TileContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/TileContainer.js
new file mode 100644
index 00000000..bfcbf735
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/TileContainer.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+import { useDispatch, useSelector } from 'react-redux'
+import { goFromRoomToRack } from '../../../redux/actions/interaction-level'
+import TileGroup from '../../../components/app/map/groups/TileGroup'
+
+const TileContainer = (props) => {
+ const interactionLevel = useSelector((state) => state.interactionLevel)
+ const tile = useSelector((state) => state.objects.tile[props.tileId])
+
+ const dispatch = useDispatch()
+ const onClick = (tile) => {
+ if (tile.rack) {
+ dispatch(goFromRoomToRack(tile._id))
+ }
+ }
+ return <TileGroup {...props} onClick={onClick} tile={tile} interactionLevel={interactionLevel} />
+}
+
+TileContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default TileContainer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/TopologyContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/TopologyContainer.js
new file mode 100644
index 00000000..78e75d0f
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/TopologyContainer.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useSelector } from 'react-redux'
+import TopologyGroup from '../../../components/app/map/groups/TopologyGroup'
+import { useActiveTopology } from '../../../data/topology'
+
+const TopologyContainer = () => {
+ const topology = useActiveTopology()
+ const interactionLevel = useSelector((state) => state.interactionLevel)
+
+ return <TopologyGroup topology={topology} interactionLevel={interactionLevel} />
+}
+
+export default TopologyContainer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/WallContainer.js b/opendc-web/opendc-web-ui/src/components/app/map/WallContainer.js
new file mode 100644
index 00000000..51dffe4b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/WallContainer.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+import { useSelector } from 'react-redux'
+import WallGroup from '../../../components/app/map/groups/WallGroup'
+
+const WallContainer = (props) => {
+ const tiles = useSelector((state) =>
+ state.objects.room[props.roomId].tiles.map((tileId) => state.objects.tile[tileId])
+ )
+ return <WallGroup {...props} tiles={tiles} />
+}
+
+WallContainer.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default WallContainer
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.js
new file mode 100644
index 00000000..f54b7c84
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { ChevronLeftIcon } from '@patternfly/react-icons'
+import { collapseContainer } from './Collapse.module.scss'
+import { Button } from '@patternfly/react-core'
+
+function Collapse({ onClick }) {
+ return (
+ <div className={collapseContainer}>
+ <Button variant="tertiary" onClick={onClick}>
+ <ChevronLeftIcon />
+ </Button>
+ </div>
+ )
+}
+
+Collapse.propTypes = {
+ onClick: PropTypes.func,
+}
+
+export default Collapse
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.module.scss b/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.module.scss
new file mode 100644
index 00000000..0c1fac94
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/Collapse.module.scss
@@ -0,0 +1,55 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.collapseContainer {
+ position: absolute;
+ right: var(--pf-global--spacer--xs);
+ top: 0;
+ bottom: 10%;
+ margin: auto 0;
+ height: 50px;
+
+ button:global(.pf-m-tertiary) {
+ height: 100%;
+ padding: 2px;
+
+ margin-right: var(--pf-global--spacer--xs);
+ margin-top: var(--pf-global--spacer--xs);
+ background-color: var(--pf-global--BackgroundColor--100);
+ border: none;
+ border-radius: var(--pf-global--BorderRadius--sm);
+ box-shadow: var(--pf-global--BoxShadow--sm);
+
+ &:not(:global(.pf-m-disabled)) {
+ background-color: var(--pf-global--BackgroundColor--100);
+ }
+
+ &:after {
+ display: none;
+ }
+
+ &:hover {
+ border: none;
+ box-shadow: var(--pf-global--BoxShadow--md);
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js
deleted file mode 100644
index 9e8cb36a..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faCamera } from '@fortawesome/free-solid-svg-icons'
-
-const ExportCanvasComponent = () => (
- <button
- className="btn btn-success btn-circle btn-sm"
- title="Export Canvas to PNG Image"
- onClick={() => window['exportCanvasToImage']()}
- >
- <FontAwesomeIcon icon={faCamera} />
- </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/ScaleIndicator.js
index ef633764..11c2f2d3 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicator.js
@@ -1,16 +1,16 @@
import PropTypes from 'prop-types'
import React from 'react'
import { TILE_SIZE_IN_METERS, TILE_SIZE_IN_PIXELS } from '../MapConstants'
-import { scaleIndicator } from './ScaleIndicatorComponent.module.scss'
+import { scaleIndicator } from './ScaleIndicator.module.scss'
-const ScaleIndicatorComponent = ({ scale }) => (
+const ScaleIndicator = ({ scale }) => (
<div className={scaleIndicator} style={{ width: TILE_SIZE_IN_PIXELS * scale }}>
{TILE_SIZE_IN_METERS}m
</div>
)
-ScaleIndicatorComponent.propTypes = {
+ScaleIndicator.propTypes = {
scale: PropTypes.number.isRequired,
}
-export default ScaleIndicatorComponent
+export default ScaleIndicator
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.module.scss b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicator.module.scss
index f19e0ff2..f19e0ff2 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.module.scss
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicator.module.scss
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
deleted file mode 100644
index d2f70953..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react'
-import ZoomControlContainer from '../../../../containers/app/map/controls/ZoomControlContainer'
-import ExportCanvasComponent from './ExportCanvasComponent'
-import { toolPanel } from './ToolPanelComponent.module.scss'
-
-const ToolPanelComponent = () => (
- <div className={toolPanel}>
- <ZoomControlContainer />
- <ExportCanvasComponent />
- </div>
-)
-
-export default ToolPanelComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.module.scss b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.module.scss
deleted file mode 100644
index 970b1ce2..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.module.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.toolPanel {
- position: absolute;
- left: 10px;
- bottom: 10px;
- z-index: 50;
-}
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.js
new file mode 100644
index 00000000..4c60bfb2
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.js
@@ -0,0 +1,28 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { control, toolBar } from './Toolbar.module.scss'
+import { Button } from '@patternfly/react-core'
+import { SearchPlusIcon, SearchMinusIcon } from '@patternfly/react-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faCamera } from '@fortawesome/free-solid-svg-icons'
+
+const Toolbar = ({ onZoom, onExport }) => (
+ <div className={toolBar}>
+ <Button variant="tertiary" title="Zoom in" onClick={() => onZoom(true)} className={control}>
+ <SearchPlusIcon />
+ </Button>
+ <Button variant="tertiary" title="Zoom out" onClick={() => onZoom(false)} className={control}>
+ <SearchMinusIcon />
+ </Button>
+ <Button variant="tertiary" title="Export Canvas to PNG Image" onClick={() => onExport()} className={control}>
+ <FontAwesomeIcon icon={faCamera} />
+ </Button>
+ </div>
+)
+
+Toolbar.propTypes = {
+ onZoom: PropTypes.func,
+ onExport: PropTypes.func,
+}
+
+export default Toolbar
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.module.scss b/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.module.scss
new file mode 100644
index 00000000..0d505acc
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/Toolbar.module.scss
@@ -0,0 +1,29 @@
+.toolBar {
+ position: absolute;
+ bottom: var(--pf-global--spacer--md);
+ left: var(--pf-global--spacer--xl);
+}
+
+.control {
+ &:global(.pf-m-tertiary) {
+ margin-right: var(--pf-global--spacer--xs);
+ margin-top: var(--pf-global--spacer--xs);
+ background-color: var(--pf-global--BackgroundColor--100);
+ border: none;
+ border-radius: var(--pf-global--BorderRadius--sm);
+ box-shadow: var(--pf-global--BoxShadow--sm);
+
+ &:not(:global(.pf-m-disabled)) {
+ background-color: var(--pf-global--BackgroundColor--100);
+ }
+
+ &:after {
+ display: none;
+ }
+
+ &:hover {
+ border: none;
+ box-shadow: var(--pf-global--BoxShadow--md);
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js
deleted file mode 100644
index 6c3c6cb7..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPlus, faMinus } from '@fortawesome/free-solid-svg-icons'
-
-const ZoomControlComponent = ({ zoomInOnCenter }) => {
- return (
- <span>
- <button
- className="btn btn-default btn-circle btn-sm mr-1"
- title="Zoom in"
- onClick={() => zoomInOnCenter(true)}
- >
- <FontAwesomeIcon icon={faPlus} />
- </button>
- <button
- className="btn btn-default btn-circle btn-sm mr-1"
- title="Zoom out"
- onClick={() => zoomInOnCenter(false)}
- >
- <FontAwesomeIcon icon={faMinus} />
- </button>
- </span>
- )
-}
-
-ZoomControlComponent.propTypes = {
- zoomInOnCenter: PropTypes.func.isRequired,
-}
-
-export default ZoomControlComponent
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
index 40e28f01..9c4abc4a 100644
--- 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
@@ -1,10 +1,10 @@
import React from 'react'
import { Group } from 'react-konva'
-import RackEnergyFillContainer from '../../../../containers/app/map/RackEnergyFillContainer'
-import RackSpaceFillContainer from '../../../../containers/app/map/RackSpaceFillContainer'
import { Tile } from '../../../../shapes'
import { RACK_BACKGROUND_COLOR } from '../../../../util/colors'
import TileObject from '../elements/TileObject'
+import RackSpaceFillContainer from '../RackSpaceFillContainer'
+import RackEnergyFillContainer from '../RackEnergyFillContainer'
const RackGroup = ({ tile }) => {
return (
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
index 42d20ff1..a14f3676 100644
--- 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
@@ -1,10 +1,10 @@
import PropTypes from 'prop-types'
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 { InteractionLevel, Room } from '../../../../shapes'
+import GrayContainer from '../GrayContainer'
+import TileContainer from '../TileContainer'
+import WallContainer from '../WallContainer'
const RoomGroup = ({ room, interactionLevel, currentRoomInConstruction, onClick }) => {
if (currentRoomInConstruction === room._id) {
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
index ce5e4a6b..cd36c7e5 100644
--- 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
@@ -1,10 +1,10 @@
import PropTypes from 'prop-types'
import React from 'react'
import { Group } from 'react-konva'
-import RackContainer from '../../../../containers/app/map/RackContainer'
import { Tile } from '../../../../shapes'
import { ROOM_DEFAULT_COLOR, ROOM_IN_CONSTRUCTION_COLOR } from '../../../../util/colors'
import RoomTile from '../elements/RoomTile'
+import RackContainer from '../RackContainer'
const TileGroup = ({ tile, newTile, onClick }) => {
let tileObject
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
index d4c6db7d..d3bcb279 100644
--- 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
@@ -1,8 +1,8 @@
import React from 'react'
import { Group } from 'react-konva'
-import GrayContainer from '../../../../containers/app/map/GrayContainer'
-import RoomContainer from '../../../../containers/app/map/RoomContainer'
import { InteractionLevel, Topology } from '../../../../shapes'
+import RoomContainer from '../RoomContainer'
+import GrayContainer from '../GrayContainer'
const TopologyGroup = ({ topology, interactionLevel }) => {
if (!topology) {
diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js
new file mode 100644
index 00000000..badb9f68
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayer.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import MapLayerComponent from '../../../../components/app/map/layers/MapLayerComponent'
+import { useMapPosition, useMapScale } from '../../../../data/map'
+
+const MapLayer = (props) => {
+ const position = useMapPosition()
+ const scale = useMapScale()
+ return <MapLayerComponent {...props} mapPosition={position} mapScale={scale} />
+}
+
+export default MapLayer
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
index 96e6867c..efe5b4e5 100644
--- 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
@@ -1,9 +1,9 @@
import PropTypes from 'prop-types'
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'
+import TopologyContainer from '../TopologyContainer'
const MapLayerComponent = ({ mapPosition, mapScale }) => (
<Layer>
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayer.js
index e9a64545..9a087bd5 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayer.js
@@ -1,3 +1,25 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { addRackToTile } from '../../../../redux/actions/topology/room'
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayer.js
index 4070c766..87240813 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayer.js
@@ -1,3 +1,25 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { toggleTileAtLocation } from '../../../../redux/actions/topology/building'
diff --git a/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultInfo.js b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultInfo.js
new file mode 100644
index 00000000..09348e60
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultInfo.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { Tooltip } from '@patternfly/react-core'
+import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'
+import { METRIC_DESCRIPTIONS } from '../../../util/available-metrics'
+
+function PortfolioResultInfo({ metric }) {
+ return (
+ <Tooltip position="top" content={<div>{METRIC_DESCRIPTIONS[metric]}</div>}>
+ <OutlinedQuestionCircleIcon title="Metric information" />
+ </Tooltip>
+ )
+}
+
+PortfolioResultInfo.propTypes = {
+ metric: PropTypes.string.isRequired,
+}
+
+export default PortfolioResultInfo
diff --git a/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResults.js b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResults.js
new file mode 100644
index 00000000..6a96c70c
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResults.js
@@ -0,0 +1,134 @@
+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, METRIC_NAMES_SHORT, METRIC_UNITS } from '../../../util/available-metrics'
+import { mean, std } from 'mathjs'
+import approx from 'approximate-number'
+import {
+ Bullseye,
+ Card,
+ CardActions,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ Grid,
+ GridItem,
+ Spinner,
+ Title,
+} from '@patternfly/react-core'
+import { ErrorCircleOIcon, CubesIcon } from '@patternfly/react-icons'
+import { usePortfolioScenarios } from '../../../data/project'
+import NewScenario from '../../projects/NewScenario'
+import PortfolioResultInfo from './PortfolioResultInfo'
+
+const PortfolioResults = ({ portfolioId }) => {
+ const { status, data: scenarios = [] } = usePortfolioScenarios(portfolioId)
+
+ if (status === 'loading') {
+ return (
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={Spinner} />
+ <Title size="lg" headingLevel="h4">
+ Loading Results
+ </Title>
+ </EmptyState>
+ </Bullseye>
+ )
+ } else if (status === 'error') {
+ return (
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={ErrorCircleOIcon} />
+ <Title size="lg" headingLevel="h4">
+ Unable to connect
+ </Title>
+ <EmptyStateBody>
+ There was an error retrieving data. Check your connection and try again.
+ </EmptyStateBody>
+ </EmptyState>
+ </Bullseye>
+ )
+ } else if (scenarios.length === 0) {
+ return (
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={CubesIcon} />
+ <Title size="lg" headingLevel="h4">
+ No results
+ </Title>
+ <EmptyStateBody>
+ No results are currently available for this portfolio. Run a scenario to obtain simulation
+ results.
+ </EmptyStateBody>
+ <NewScenario portfolioId={portfolioId} />
+ </EmptyState>
+ </Bullseye>
+ )
+ }
+
+ const dataPerMetric = {}
+
+ AVAILABLE_METRICS.forEach((metric) => {
+ dataPerMetric[metric] = scenarios
+ .filter((scenario) => scenario.results)
+ .map((scenario) => ({
+ name: scenario.name,
+ value: mean(scenario.results[metric]),
+ errorX: std(scenario.results[metric]),
+ }))
+ })
+
+ return (
+ <Grid hasGutter>
+ {AVAILABLE_METRICS.map((metric) => (
+ <GridItem xl={6} lg={12} key={metric}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <PortfolioResultInfo metric={metric} />
+ </CardActions>
+ <CardTitle>{METRIC_NAMES[metric]}</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <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>
+ </CardBody>
+ </Card>
+ </GridItem>
+ ))}
+ </Grid>
+ )
+}
+
+PortfolioResults.propTypes = {
+ portfolioId: PropTypes.string,
+}
+
+export default PortfolioResults
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
deleted file mode 100644
index 983a5c1d..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js
+++ /dev/null
@@ -1,93 +0,0 @@
-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 { Portfolio, Scenario } from '../../../shapes'
-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: Portfolio,
- scenarios: PropTypes.arrayOf(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
deleted file mode 100644
index 56fa799f..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import PropTypes from 'prop-types'
-import classNames from 'classnames'
-import React, { useState } from 'react'
-import { collapseButton, collapseButtonRight, sidebar, sidebarRight } from './Sidebar.module.scss'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons'
-
-function Sidebar({ isRight, collapsible = true, children }) {
- const [isCollapsed, setCollapsed] = useState(false)
-
- const button = (
- <div
- className={classNames(collapseButton, {
- [collapseButtonRight]: isRight,
- })}
- onClick={() => setCollapsed(!isCollapsed)}
- >
- {(isCollapsed && isRight) || (!isCollapsed && !isRight) ? (
- <FontAwesomeIcon icon={faAngleLeft} title={isRight ? 'Expand' : 'Collapse'} />
- ) : (
- <FontAwesomeIcon icon={faAngleRight} title={isRight ? 'Collapse' : 'Expand'} />
- )}
- </div>
- )
-
- if (isCollapsed) {
- return button
- }
- return (
- <div
- className={classNames(`${sidebar} p-3 h-100`, {
- [sidebarRight]: isRight,
- })}
- onWheel={(e) => e.stopPropagation()}
- >
- {children}
- {collapsible && button}
- </div>
- )
-}
-
-Sidebar.propTypes = {
- isRight: PropTypes.bool.isRequired,
- collapsible: PropTypes.bool,
- children: PropTypes.node,
-}
-
-export default Sidebar
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.module.scss b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.module.scss
deleted file mode 100644
index 19c6a97f..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.module.scss
+++ /dev/null
@@ -1,57 +0,0 @@
-@import 'src/style/_variables.scss';
-@import 'src/style/_mixins.scss';
-
-.collapseButton {
- position: absolute;
- left: 5px;
- top: 5px;
- padding: 5px 7px;
-
- background: white;
- border: solid 1px $gray-semi-light;
- z-index: 99;
-
- @include clickable;
- border-radius: 5px;
- transition: background 200ms;
-
- &.collapseButtonRight {
- 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;
-
- .collapseButton {
- left: auto;
- right: -25px;
- }
-}
-
-.sidebarRight {
- left: auto;
- right: 0;
-
- border-left: $gray-semi-dark 1px solid;
- border-right: none;
-
- .collapseButtonRight {
- 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
deleted file mode 100644
index d61ff24e..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { Portfolio } from '../../../../shapes'
-import Link from 'next/link'
-import ScenarioListContainer from '../../../../containers/app/sidebars/project/ScenarioListContainer'
-import { Button, Col, Row } from 'reactstrap'
-import classNames from 'classnames'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPlus, faPlay, faTrash } from '@fortawesome/free-solid-svg-icons'
-
-function PortfolioListComponent({
- portfolios,
- currentProjectId,
- currentPortfolioId,
- onNewPortfolio,
- onChoosePortfolio,
- onDeletePortfolio,
-}) {
- return (
- <div className="pb-3">
- <h2>
- Portfolios
- <Button color="primary" outline className="float-right" onClick={(e) => onNewPortfolio(e)}>
- <FontAwesomeIcon icon={faPlus} />
- </Button>
- </h2>
-
- {portfolios.map((portfolio) => (
- <div key={portfolio._id}>
- <Row className="row mb-1">
- <Col
- xs="7"
- className={classNames('align-self-center', {
- 'font-weight-bold': portfolio._id === currentPortfolioId,
- })}
- >
- {portfolio.name}
- </Col>
- <Col xs="5" className="text-right">
- <Link passHref href={`/projects/${currentProjectId}/portfolios/${portfolio._id}`}>
- <Button
- color="primary"
- outline
- className="mr-1"
- onClick={() => onChoosePortfolio(portfolio._id)}
- >
- <FontAwesomeIcon icon={faPlay} />
- </Button>
- </Link>
- <Button color="danger" outline onClick={() => onDeletePortfolio(portfolio._id)}>
- <FontAwesomeIcon icon={faTrash} />
- </Button>
- </Col>
- </Row>
- <ScenarioListContainer portfolioId={portfolio._id} />
- </div>
- ))}
- </div>
- )
-}
-
-PortfolioListComponent.propTypes = {
- portfolios: PropTypes.arrayOf(Portfolio),
- currentProjectId: PropTypes.string,
- currentPortfolioId: PropTypes.string,
- onNewPortfolio: PropTypes.func.isRequired,
- onChoosePortfolio: PropTypes.func.isRequired,
- onDeletePortfolio: PropTypes.func.isRequired,
-}
-
-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
deleted file mode 100644
index 10d22e5b..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import Sidebar from '../Sidebar'
-import TopologyListContainer from '../../../../containers/app/sidebars/project/TopologyListContainer'
-import PortfolioListContainer from '../../../../containers/app/sidebars/project/PortfolioListContainer'
-import { Container } from 'reactstrap'
-
-const ProjectSidebarComponent = ({ collapsible }) => (
- <Sidebar isRight={false} collapsible={collapsible}>
- <Container fluid className="h-100 overflow-auto">
- <TopologyListContainer />
- <PortfolioListContainer />
- </Container>
- </Sidebar>
-)
-
-ProjectSidebarComponent.propTypes = {
- collapsible: PropTypes.bool,
-}
-
-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
deleted file mode 100644
index e81d2b78..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { Scenario } from '../../../../shapes'
-import { Button, Col, Row } from 'reactstrap'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'
-
-function ScenarioListComponent({ scenarios, portfolioId, onNewScenario, onDeleteScenario }) {
- return (
- <>
- {scenarios.map((scenario, idx) => (
- <Row key={scenario._id} className="mb-1">
- <Col xs="7" className="pl-5 align-self-center">
- {scenario.name}
- </Col>
- <Col xs="5" className="text-right">
- <Button
- color="danger"
- outline
- disabled={idx === 0}
- onClick={() => (idx !== 0 ? onDeleteScenario(scenario._id) : undefined)}
- >
- <FontAwesomeIcon icon={faTrash} />
- </Button>
- </Col>
- </Row>
- ))}
- <div className="pl-4 mb-2">
- <Button color="primary" outline onClick={() => onNewScenario(portfolioId)}>
- <FontAwesomeIcon icon={faPlus} className="mr-1" />
- New scenario
- </Button>
- </div>
- </>
- )
-}
-
-ScenarioListComponent.propTypes = {
- scenarios: PropTypes.arrayOf(Scenario),
- portfolioId: PropTypes.string,
- onNewScenario: PropTypes.func.isRequired,
- onDeleteScenario: PropTypes.func.isRequired,
-}
-
-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
deleted file mode 100644
index ac58669b..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { Topology } from '../../../../shapes'
-import { Button, Col, Row } from 'reactstrap'
-import classNames from 'classnames'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPlus, faPlay, faTrash } from '@fortawesome/free-solid-svg-icons'
-
-function TopologyListComponent({ topologies, currentTopologyId, onChooseTopology, onNewTopology, onDeleteTopology }) {
- return (
- <div className="pb-3">
- <h2>
- Topologies
- <Button color="primary" outline className="float-right" onClick={onNewTopology}>
- <FontAwesomeIcon icon={faPlus} />
- </Button>
- </h2>
-
- {topologies.map((topology, idx) => (
- <Row key={topology._id} className="mb-1">
- <Col
- xs="7"
- className={classNames('align-self-center', {
- 'font-weight-bold': topology._id === currentTopologyId,
- })}
- >
- {topology.name}
- </Col>
- <Col xs="5" className="text-right">
- <Button color="primary" outline className="mr-1" onClick={() => onChooseTopology(topology._id)}>
- <FontAwesomeIcon icon={faPlay} />
- </Button>
- <Button
- color="danger"
- outline
- disabled={idx === 0}
- onClick={() => (idx !== 0 ? onDeleteTopology(topology._id) : undefined)}
- >
- <FontAwesomeIcon icon={faTrash} />
- </Button>
- </Col>
- </Row>
- ))}
- </div>
- )
-}
-
-TopologyListComponent.propTypes = {
- topologies: PropTypes.arrayOf(Topology),
- currentTopologyId: PropTypes.string,
- onChooseTopology: PropTypes.func.isRequired,
- onNewTopology: PropTypes.func.isRequired,
- onDeleteTopology: PropTypes.func.isRequired,
-}
-
-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
index b8c88003..ececd07b 100644
--- 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
@@ -1,16 +1,65 @@
import PropTypes from 'prop-types'
-import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPencilAlt } from '@fortawesome/free-solid-svg-icons'
-
-const NameComponent = ({ name, onEdit }) => (
- <h2>
- {name}
- <button className="btn btn-outline-secondary float-right" onClick={onEdit}>
- <FontAwesomeIcon icon={faPencilAlt} />
- </button>
- </h2>
-)
+import React, { useRef, useState } from 'react'
+import { Button, TextInput } from '@patternfly/react-core'
+import { PencilAltIcon, CheckIcon, TimesIcon } from '@patternfly/react-icons'
+
+function NameComponent({ name, onEdit }) {
+ const [isEditing, setEditing] = useState(false)
+ const nameInput = useRef(null)
+
+ const onCancel = () => {
+ nameInput.current.value = name
+ setEditing(false)
+ }
+
+ const onSubmit = (event) => {
+ if (event) {
+ event.preventDefault()
+ }
+
+ const name = nameInput.current.value
+ if (name) {
+ onEdit(name)
+ }
+
+ setEditing(false)
+ }
+
+ return (
+ <form
+ className={`pf-c-inline-edit ${isEditing ? 'pf-m-inline-editable' : ''} pf-u-display-inline-block`}
+ onSubmit={onSubmit}
+ >
+ <div className="pf-c-inline-edit__group">
+ <div className="pf-c-inline-edit__value" id="single-inline-edit-example-label">
+ {name}
+ </div>
+ <div className="pf-c-inline-edit__action pf-m-enable-editable">
+ <Button className="pf-u-py-0" variant="plain" aria-label="Edit" onClick={() => setEditing(true)}>
+ <PencilAltIcon />
+ </Button>
+ </div>
+ </div>
+ <div className="pf-c-inline-edit__group">
+ <div className="pf-c-inline-edit__input">
+ <TextInput type="text" defaultValue={name} ref={nameInput} aria-label="Editable text input" />
+ </div>
+ <div className="pf-c-inline-edit__group pf-m-action-group pf-m-icon-group">
+ <div className="pf-c-inline-edit__action pf-m-valid">
+ <Button className="pf-u-py-0" variant="plain" aria-label="Save edits" onClick={onSubmit}>
+ <CheckIcon />
+ </Button>
+ </div>
+ <div className="pf-c-inline-edit__action">
+ <Button className="pf-u-py-0" variant="plain" aria-label="Cancel edits" onClick={onCancel}>
+ <TimesIcon />
+ </Button>
+ </div>
+ </div>
+ </div>
+ </form>
+ )
+}
NameComponent.propTypes = {
name: PropTypes.string,
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.js
new file mode 100644
index 00000000..c4a880b1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.js
@@ -0,0 +1,83 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { InteractionLevel } from '../../../../shapes'
+import BuildingSidebarComponent from './building/BuildingSidebarComponent'
+import {
+ Button,
+ DrawerActions,
+ DrawerCloseButton,
+ DrawerHead,
+ DrawerPanelBody,
+ DrawerPanelContent,
+ Flex,
+ Title,
+} from '@patternfly/react-core'
+import { AngleLeftIcon } from '@patternfly/react-icons'
+import { useDispatch } from 'react-redux'
+import { goDownOneInteractionLevel } from '../../../../redux/actions/interaction-level'
+import { backButton } from './TopologySidebar.module.scss'
+import RoomSidebarContainer from './room/RoomSidebarContainer'
+import RackSidebarContainer from './rack/RackSidebarContainer'
+import MachineSidebarContainer from './machine/MachineSidebarContainer'
+
+const name = {
+ BUILDING: 'Building',
+ ROOM: 'Room',
+ RACK: 'Rack',
+ MACHINE: 'Machine',
+}
+
+const TopologySidebar = ({ interactionLevel, onClose }) => {
+ let sidebarContent
+
+ switch (interactionLevel.mode) {
+ case 'BUILDING':
+ sidebarContent = <BuildingSidebarComponent />
+ break
+ case 'ROOM':
+ sidebarContent = <RoomSidebarContainer />
+ break
+ case 'RACK':
+ sidebarContent = <RackSidebarContainer />
+ break
+ case 'MACHINE':
+ sidebarContent = <MachineSidebarContainer />
+ break
+ default:
+ sidebarContent = 'Missing Content'
+ }
+
+ const dispatch = useDispatch()
+ const onClick = () => dispatch(goDownOneInteractionLevel())
+
+ return (
+ <DrawerPanelContent isResizable defaultSize="450px" minSize="400px">
+ <DrawerHead>
+ <Flex>
+ <Button
+ variant="tertiary"
+ isSmall
+ className={backButton}
+ onClick={interactionLevel.mode === 'BUILDING' ? onClose : onClick}
+ >
+ <AngleLeftIcon />
+ </Button>
+ <Title className="pf-u-align-self-center" headingLevel="h1">
+ {name[interactionLevel.mode]}
+ </Title>
+ </Flex>
+ <DrawerActions>
+ <DrawerCloseButton onClose={onClose} />
+ </DrawerActions>
+ </DrawerHead>
+ <DrawerPanelBody>{sidebarContent}</DrawerPanelBody>
+ </DrawerPanelContent>
+ )
+}
+
+TopologySidebar.propTypes = {
+ interactionLevel: InteractionLevel,
+ onClose: PropTypes.func,
+}
+
+export default TopologySidebar
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.module.scss b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.module.scss
new file mode 100644
index 00000000..45dc98da
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebar.module.scss
@@ -0,0 +1,37 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.backButton {
+ &:global(.pf-c-button) {
+ align-self: center;
+ --pf-c-button--after--BorderColor: var(--pf-global--BorderColor--light-100);
+ color: var(--pf-global--Color--400);
+
+ --pf-c-button--PaddingRight: var(--pf-global--spacer--sm);
+ --pf-c-button--PaddingLeft: var(--pf-global--spacer--sm);
+
+ &:hover,
+ &:focus {
+ --pf-c-button--after--BorderColor: var(--pf-global--BorderColor--100);
+ }
+ }
+}
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
deleted file mode 100644
index 450df6cd..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js
+++ /dev/null
@@ -1,36 +0,0 @@
-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'
-import { InteractionLevel } from '../../../../shapes'
-
-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>
-}
-
-TopologySidebarComponent.propTypes = {
- interactionLevel: InteractionLevel,
-}
-
-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
index eea62f84..6c2556d3 100644
--- 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
@@ -1,13 +1,8 @@
import React from 'react'
-import NewRoomConstructionContainer from '../../../../../containers/app/sidebars/topology/building/NewRoomConstructionContainer'
+import NewRoomConstructionContainer from './NewRoomConstructionContainer'
const BuildingSidebarComponent = () => {
- return (
- <div>
- <h2>Building</h2>
- <NewRoomConstructionContainer />
- </div>
- )
+ return <NewRoomConstructionContainer />
}
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
index e8c81735..656b2515 100644
--- 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
@@ -1,29 +1,38 @@
import PropTypes from 'prop-types'
import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPlus, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons'
-import { Button } from 'reactstrap'
+import { Button, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'
+import PlusIcon from '@patternfly/react-icons/dist/js/icons/plus-icon'
+import CheckIcon from '@patternfly/react-icons/dist/js/icons/check-icon'
const NewRoomConstructionComponent = ({ onStart, onFinish, onCancel, currentRoomInConstruction }) => {
if (currentRoomInConstruction === '-1') {
return (
- <div className="btn btn-outline-primary btn-block" onClick={onStart}>
- <FontAwesomeIcon icon={faPlus} className="mr-2" />
+ <Button isBlock icon={<PlusIcon />} onClick={onStart}>
Construct a new room
- </div>
+ </Button>
)
}
return (
- <div>
- <Button color="primary" block onClick={onFinish}>
- <FontAwesomeIcon icon={faCheck} className="mr-2" />
- Finalize new room
- </Button>
- <Button color="default" block onClick={onCancel}>
- <FontAwesomeIcon icon={faTimes} className="mr-2" />
- Cancel construction
- </Button>
- </div>
+ <Toolbar
+ inset={{
+ default: 'insetNone',
+ }}
+ >
+ <ToolbarContent>
+ <ToolbarGroup>
+ <ToolbarItem>
+ <Button icon={<CheckIcon />} onClick={onFinish}>
+ Finalize new room
+ </Button>
+ </ToolbarItem>
+ <ToolbarItem widths={{ default: '100%' }}>
+ <Button isBlock variant="secondary" onClick={onCancel}>
+ Cancel
+ </Button>
+ </ToolbarItem>
+ </ToolbarGroup>
+ </ToolbarContent>
+ </Toolbar>
)
}
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionContainer.js
new file mode 100644
index 00000000..0836263c
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionContainer.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import {
+ cancelNewRoomConstruction,
+ finishNewRoomConstruction,
+ startNewRoomConstruction,
+} from '../../../../../redux/actions/topology/building'
+import NewRoomConstructionComponent from './NewRoomConstructionComponent'
+
+const NewRoomConstructionButton = (props) => {
+ const currentRoomInConstruction = useSelector((state) => state.construction.currentRoomInConstruction)
+
+ const dispatch = useDispatch()
+ const actions = {
+ onStart: () => dispatch(startNewRoomConstruction()),
+ onFinish: () => dispatch(finishNewRoomConstruction()),
+ onCancel: () => dispatch(cancelNewRoomConstruction()),
+ }
+ return (
+ <NewRoomConstructionComponent {...props} {...actions} currentRoomInConstruction={currentRoomInConstruction} />
+ )
+}
+
+export default NewRoomConstructionButton
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
deleted file mode 100644
index 829bf265..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faAngleLeft } from '@fortawesome/free-solid-svg-icons'
-
-const BackToRackComponent = ({ onClick }) => (
- <div className="btn btn-secondary btn-block" onClick={onClick}>
- <FontAwesomeIcon icon={faAngleLeft} className="mr-2" />
- Back to rack
- </div>
-)
-
-BackToRackComponent.propTypes = {
- onClick: PropTypes.func,
-}
-
-export default BackToRackComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachine.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachine.js
new file mode 100644
index 00000000..a7bf3719
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachine.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { useState } from 'react'
+import { useDispatch } from 'react-redux'
+import { deleteMachine } from '../../../../../redux/actions/topology/machine'
+import { Button } from '@patternfly/react-core'
+import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon'
+import ConfirmationModal from '../../../../modals/ConfirmationModal'
+
+const DeleteMachine = () => {
+ const dispatch = useDispatch()
+ const [isVisible, setVisible] = useState(false)
+ const callback = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteMachine())
+ }
+ setVisible(false)
+ }
+ return (
+ <>
+ <Button variant="danger" icon={<TrashIcon />} isBlock onClick={() => setVisible(true)}>
+ Delete this machine
+ </Button>
+ <ConfirmationModal
+ title="Delete this machine"
+ message="Are you sure you want to delete this machine?"
+ isOpen={isVisible}
+ callback={callback}
+ />
+ </>
+ )
+}
+
+export default DeleteMachine
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
index 88a99e0f..d3d4a8cf 100644
--- 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
@@ -1,17 +1,38 @@
import PropTypes from 'prop-types'
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'
+import UnitTabsComponent from './UnitTabsComponent'
+import DeleteMachine from './DeleteMachine'
+import {
+ TextContent,
+ TextList,
+ TextListItem,
+ TextListItemVariants,
+ TextListVariants,
+ Title,
+} from '@patternfly/react-core'
+import { useSelector } from 'react-redux'
const MachineSidebarComponent = ({ machineId }) => {
+ const machine = useSelector((state) => state.objects.machine[machineId])
return (
- <div className="h-100 overflow-auto">
- <MachineNameContainer />
- <BackToRackContainer />
- <DeleteMachineContainer />
- <UnitTabsContainer />
+ <div>
+ <TextContent>
+ <Title headingLevel="h2">Details</Title>
+ <TextList component={TextListVariants.dl}>
+ <TextListItem component={TextListItemVariants.dt}>Name</TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>
+ Machine at position {machine.position}
+ </TextListItem>
+ </TextList>
+
+ <Title headingLevel="h2">Actions</Title>
+ <DeleteMachine />
+
+ <Title headingLevel="h2">Units</Title>
+ </TextContent>
+ <div className="pf-u-h-100">
+ <UnitTabsComponent />
+ </div>
</div>
)
}
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarContainer.js
new file mode 100644
index 00000000..94d9f2c3
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarContainer.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useSelector } from 'react-redux'
+import MachineSidebarComponent from './MachineSidebarComponent'
+
+const MachineSidebarContainer = (props) => {
+ const machineId = useSelector(
+ (state) =>
+ state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].machines[
+ state.interactionLevel.position - 1
+ ]
+ )
+ return <MachineSidebarComponent {...props} machineId={machineId} />
+}
+
+export default MachineSidebarContainer
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
index 532add37..88591208 100644
--- 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
@@ -1,28 +1,36 @@
import PropTypes from 'prop-types'
-import React, { useRef } from 'react'
-import { Button, Form, FormGroup, Input } from 'reactstrap'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPlus } from '@fortawesome/free-solid-svg-icons'
+import React, { useState } from 'react'
+import { Button, InputGroup, Select, SelectOption, SelectVariant } from '@patternfly/react-core'
+import PlusIcon from '@patternfly/react-icons/dist/js/icons/plus-icon'
function UnitAddComponent({ units, onAdd }) {
- const unitSelect = useRef(null)
+ const [isOpen, setOpen] = useState(false)
+ const [selected, setSelected] = useState(null)
return (
- <Form inline>
- <FormGroup className="w-100">
- <Input type="select" className="w-70 mr-1" innerRef={unitSelect}>
- {units.map((unit) => (
- <option value={unit._id} key={unit._id}>
- {unit.name}
- </option>
- ))}
- </Input>
- <Button color="primary" outline onClick={() => onAdd(unitSelect.current.value)}>
- <FontAwesomeIcon icon={faPlus} className="mr-2" />
- Add
- </Button>
- </FormGroup>
- </Form>
+ <InputGroup>
+ <Select
+ variant={SelectVariant.single}
+ placeholderText="Select a unit"
+ aria-label="Select Unit"
+ onToggle={() => setOpen(!isOpen)}
+ isOpen={isOpen}
+ onSelect={(_, selection) => {
+ setSelected(selection)
+ setOpen(false)
+ }}
+ selections={selected}
+ >
+ {units.map((unit) => (
+ <SelectOption value={unit._id} key={unit._id}>
+ {unit.name}
+ </SelectOption>
+ ))}
+ </Select>
+ <Button icon={<PlusIcon />} variant="control" onClick={() => onAdd(selected)}>
+ Add
+ </Button>
+ </InputGroup>
)
}
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddContainer.js
new file mode 100644
index 00000000..8a6680e6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddContainer.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { addUnit } from '../../../../../redux/actions/topology/machine'
+import UnitAddComponent from './UnitAddComponent'
+
+const UnitAddContainer = ({ unitType }) => {
+ const units = useSelector((state) => Object.values(state.objects[unitType]))
+ const dispatch = useDispatch()
+
+ const onAdd = (id) => dispatch(addUnit(unitType, id))
+
+ return <UnitAddComponent onAdd={onAdd} units={units} />
+}
+
+UnitAddContainer.propTypes = {
+ unitType: PropTypes.string.isRequired,
+}
+
+export default UnitAddContainer
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
deleted file mode 100644
index 46c639bd..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { UncontrolledPopover, PopoverHeader, PopoverBody, Button } from 'reactstrap'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faTrash, faInfoCircle } from '@fortawesome/free-solid-svg-icons'
-import { ProcessingUnit, StorageUnit } from '../../../../../shapes'
-
-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" id={`unit-${index}`}>
- <FontAwesomeIcon icon={faInfoCircle} />
- </Button>
- <UncontrolledPopover trigger="focus" placement="left" target={`unit-${index}`}>
- <PopoverHeader>Unit Information</PopoverHeader>
- <PopoverBody>{unitInfo}</PopoverBody>
- </UncontrolledPopover>
-
- <Button outline color="danger" onClick={onDelete}>
- <FontAwesomeIcon icon={faTrash} />
- </Button>
- </span>
- </li>
- )
-}
-
-UnitComponent.propTypes = {
- index: PropTypes.number,
- unitType: PropTypes.string,
- unit: PropTypes.oneOfType([ProcessingUnit, StorageUnit]),
- onDelete: PropTypes.func,
-}
-
-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
index 54c1a6cc..9c3c08fd 100644
--- 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
@@ -1,29 +1,107 @@
import PropTypes from 'prop-types'
import React from 'react'
import { ProcessingUnit, StorageUnit } from '../../../../../shapes'
-import UnitComponent from './UnitComponent'
+import {
+ Button,
+ DataList,
+ DataListAction,
+ DataListCell,
+ DataListItem,
+ DataListItemCells,
+ DataListItemRow,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ Popover,
+ Title,
+} from '@patternfly/react-core'
+import { CubesIcon, InfoIcon, TrashIcon } from '@patternfly/react-icons'
-const UnitListComponent = ({ unitType, units, onDelete }) => (
- <ul className="list-group mt-1">
- {units.length !== 0 ? (
- units.map((unit, index) => (
- <UnitComponent
- unitType={unitType}
- unit={unit}
- onDelete={() => onDelete(unit, unitType)}
- index={index}
- key={index}
- />
- ))
- ) : (
- <div className="alert alert-info">
- <span>
- <strong>No units...</strong> Add some with the menu above!
- </span>
- </div>
- )}
- </ul>
-)
+const UnitInfo = ({ unit, unitType }) => {
+ if (unitType === 'cpu' || unitType === 'gpu') {
+ return (
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Clock Frequency</DescriptionListTerm>
+ <DescriptionListDescription>{unit.clockRateMhz} MHz</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Number of Cores</DescriptionListTerm>
+ <DescriptionListDescription>{unit.numberOfCores}</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Energy Consumption</DescriptionListTerm>
+ <DescriptionListDescription>{unit.energyConsumptionW} W</DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ )
+ }
+
+ return (
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Speed</DescriptionListTerm>
+ <DescriptionListDescription>{unit.speedMbPerS} Mb/s</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Capacity</DescriptionListTerm>
+ <DescriptionListDescription>{unit.sizeMb} MB</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Energy Consumption</DescriptionListTerm>
+ <DescriptionListDescription>{unit.energyConsumptionW} W</DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ )
+}
+
+UnitInfo.propTypes = {
+ unitType: PropTypes.string.isRequired,
+ unit: PropTypes.oneOfType([ProcessingUnit, StorageUnit]).isRequired,
+}
+
+const UnitListComponent = ({ unitType, units, onDelete }) => {
+ if (units.length === 0) {
+ return (
+ <EmptyState>
+ <EmptyStateIcon icon={CubesIcon} />
+ <Title headingLevel="h5" size="lg">
+ No units found
+ </Title>
+ <EmptyStateBody>You have not configured any units yet. Add some with the menu above!</EmptyStateBody>
+ </EmptyState>
+ )
+ }
+
+ return (
+ <DataList aria-label="Machine Units" isCompact>
+ {units.map((unit, index) => (
+ <DataListItem key={index}>
+ <DataListItemRow>
+ <DataListItemCells dataListCells={[<DataListCell key="unit">{unit.name}</DataListCell>]} />
+ <DataListAction id="goto" aria-label="Goto Machine" aria-labelledby="goto">
+ <Popover
+ headerContent="Unit Information"
+ bodyContent={<UnitInfo unitType={unitType} unit={unit} />}
+ >
+ <Button isSmall variant="plain" className="pf-u-p-0">
+ <InfoIcon />
+ </Button>
+ </Popover>
+ <Button isSmall variant="plain" className="pf-u-p-0" onClick={() => onDelete(units[index])}>
+ <TrashIcon />
+ </Button>
+ </DataListAction>
+ </DataListItemRow>
+ </DataListItem>
+ ))}
+ </DataList>
+ )
+}
UnitListComponent.propTypes = {
unitType: PropTypes.string.isRequired,
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListContainer.js
new file mode 100644
index 00000000..2d994f97
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListContainer.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import UnitListComponent from './UnitListComponent'
+import { deleteUnit } from '../../../../../redux/actions/topology/machine'
+
+const unitMapping = {
+ cpu: 'cpus',
+ gpu: 'gpus',
+ memory: 'memories',
+ storage: 'storages',
+}
+
+const UnitListContainer = ({ unitType, ...props }) => {
+ const dispatch = useDispatch()
+ const units = useSelector((state) => {
+ const machine =
+ state.objects.machine[
+ state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].machines[
+ state.interactionLevel.position - 1
+ ]
+ ]
+ return machine[unitMapping[unitType]].map((id) => state.objects[unitType][id])
+ })
+ const onDelete = (unit, unitType) => dispatch(deleteUnit(unitType, unit._id))
+
+ return <UnitListComponent {...props} units={units} unitType={unitType} onDelete={onDelete} />
+}
+
+UnitListContainer.propTypes = {
+ unitType: PropTypes.string.isRequired,
+}
+
+export default UnitListContainer
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
index ebb5f479..723ed2e2 100644
--- 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
@@ -1,85 +1,30 @@
import React, { useState } from 'react'
-import { Nav, NavItem, NavLink, Row, TabContent, TabPane } from 'reactstrap'
-import UnitAddContainer from '../../../../../containers/app/sidebars/topology/machine/UnitAddContainer'
-import UnitListContainer from '../../../../../containers/app/sidebars/topology/machine/UnitListContainer'
+import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'
+import UnitAddContainer from './UnitAddContainer'
+import UnitListContainer from './UnitListContainer'
const UnitTabsComponent = () => {
const [activeTab, setActiveTab] = useState('cpu-units')
- const toggle = (tab) => {
- if (activeTab !== tab) setActiveTab(tab)
- }
return (
- <div className="mt-2">
- <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">
- <div className="py-2">
- <UnitAddContainer unitType="cpu" />
- <UnitListContainer unitType="cpu" />
- </div>
- </TabPane>
- <TabPane tabId="gpu-units">
- <div className="py-2">
- <UnitAddContainer unitType="gpu" />
- <UnitListContainer unitType="gpu" />
- </div>
- </TabPane>
- <TabPane tabId="memory-units">
- <div className="py-2">
- <UnitAddContainer unitType="memory" />
- <UnitListContainer unitType="memory" />
- </div>
- </TabPane>
- <TabPane tabId="storage-units">
- <div className="py-2">
- <UnitAddContainer unitType="storage" />
- <UnitListContainer unitType="storage" />
- </div>
- </TabPane>
- </TabContent>
- </div>
+ <Tabs activeKey={activeTab} onSelect={(_, tab) => setActiveTab(tab)}>
+ <Tab eventKey="cpu-units" title={<TabTitleText>CPU</TabTitleText>}>
+ <UnitAddContainer unitType="cpu" />
+ <UnitListContainer unitType="cpu" />
+ </Tab>
+ <Tab eventKey="gpu-units" title={<TabTitleText>GPU</TabTitleText>}>
+ <UnitAddContainer unitType="gpu" />
+ <UnitListContainer unitType="gpu" />
+ </Tab>
+ <Tab eventKey="memory-units" title={<TabTitleText>Memory</TabTitleText>}>
+ <UnitAddContainer unitType="memory" />
+ <UnitListContainer unitType="memory" />
+ </Tab>
+ <Tab eventKey="storage-units" title={<TabTitleText>Storage</TabTitleText>}>
+ <UnitAddContainer unitType="storage" />
+ <UnitListContainer unitType="storage" />
+ </Tab>
+ </Tabs>
)
}
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
index a330c302..c8543134 100644
--- 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
@@ -1,12 +1,10 @@
import PropTypes from 'prop-types'
import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faSave } from '@fortawesome/free-solid-svg-icons'
-import { Button } from 'reactstrap'
+import { SaveIcon } from '@patternfly/react-icons'
+import { Button } from '@patternfly/react-core'
const AddPrefabComponent = ({ onClick }) => (
- <Button color="primary" block onClick={onClick}>
- <FontAwesomeIcon icon={faSave} className="mr-2" />
+ <Button variant="primary" icon={<SaveIcon />} isBlock onClick={onClick} className="pf-u-mb-sm">
Save this rack to a prefab
</Button>
)
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabContainer.js
new file mode 100644
index 00000000..d3d9aaf5
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabContainer.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useDispatch } from 'react-redux'
+import { addPrefab } from '../../../../../redux/actions/prefabs'
+import AddPrefabComponent from './AddPrefabComponent'
+
+const AddPrefabContainer = (props) => {
+ const dispatch = useDispatch()
+ return <AddPrefabComponent {...props} onClick={() => dispatch(addPrefab('name'))} />
+}
+
+export default AddPrefabContainer
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
deleted file mode 100644
index e0eb5979..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faAngleLeft } from '@fortawesome/free-solid-svg-icons'
-import { Button } from 'reactstrap'
-
-const BackToRoomComponent = ({ onClick }) => (
- <Button color="secondary" block className="mb-2" onClick={onClick}>
- <FontAwesomeIcon icon={faAngleLeft} className="mr-2" />
- Back to room
- </Button>
-)
-
-BackToRoomComponent.propTypes = {
- onClick: PropTypes.func,
-}
-
-export default BackToRoomComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackContainer.js
new file mode 100644
index 00000000..47959f03
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackContainer.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { useState } from 'react'
+import { useDispatch } from 'react-redux'
+import ConfirmationModal from '../../../../../components/modals/ConfirmationModal'
+import { deleteRack } from '../../../../../redux/actions/topology/rack'
+import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon'
+import { Button } from '@patternfly/react-core'
+
+const DeleteRackContainer = () => {
+ const dispatch = useDispatch()
+ const [isVisible, setVisible] = useState(false)
+ const callback = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteRack())
+ }
+ setVisible(false)
+ }
+ return (
+ <>
+ <Button variant="danger" icon={<TrashIcon />} isBlock onClick={() => setVisible(true)}>
+ Delete this rack
+ </Button>
+ <ConfirmationModal
+ title="Delete this rack"
+ message="Are you sure you want to delete this rack?"
+ isOpen={isVisible}
+ callback={callback}
+ />
+ </>
+ )
+}
+
+export default DeleteRackContainer
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
deleted file mode 100644
index 63b319e0..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPlus } from '@fortawesome/free-solid-svg-icons'
-import { ListGroupItem, Badge, Button } from 'reactstrap'
-
-const EmptySlotComponent = ({ position, onAdd }) => (
- <ListGroupItem className="d-flex justify-content-between align-items-center">
- <Badge color="info" className="mr-1">
- {position}
- </Badge>
- <Button color="primary" outline onClick={onAdd}>
- <FontAwesomeIcon icon={faPlus} className="mr-2" />
- Add machine
- </Button>
- </ListGroupItem>
-)
-
-EmptySlotComponent.propTypes = {
- position: PropTypes.number,
- onAdd: PropTypes.func,
-}
-
-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
index b71918da..1617b3bf 100644
--- 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
@@ -2,18 +2,16 @@ import PropTypes from 'prop-types'
import React from 'react'
import Image from 'next/image'
import { Machine } from '../../../../../shapes'
-import { Badge, ListGroupItem } from 'reactstrap'
+import { Flex, Label } from '@patternfly/react-core'
const UnitIcon = ({ id, type }) => (
- <div className="ml-1">
- <Image
- src={'/img/topology/' + id + '-icon.png'}
- alt={'Machine contains ' + type + ' units'}
- layout="intrinsic"
- height={35}
- width={35}
- />
- </div>
+ <Image
+ src={'/img/topology/' + id + '-icon.png'}
+ alt={'Machine contains ' + type + ' units'}
+ layout="intrinsic"
+ height={24}
+ width={24}
+ />
)
UnitIcon.propTypes = {
@@ -21,34 +19,27 @@ UnitIcon.propTypes = {
type: PropTypes.string,
}
-const MachineComponent = ({ position, machine, onClick }) => {
+const MachineComponent = ({ machine, onClick }) => {
const hasNoUnits =
machine.cpus.length + machine.gpus.length + machine.memories.length + machine.storages.length === 0
return (
- <ListGroupItem
- action
- className="d-flex justify-content-between align-items-center"
- onClick={onClick}
- style={{ backgroundColor: 'white' }}
- >
- <Badge color="info" className="mr-1">
- {position}
- </Badge>
- <div className="d-inline-flex">
- {machine.cpus.length > 0 ? <UnitIcon id="cpu" type="CPU" /> : undefined}
- {machine.gpus.length > 0 ? <UnitIcon id="gpu" type="GPU" /> : undefined}
- {machine.memories.length > 0 ? <UnitIcon id="memory" type="memory" /> : undefined}
- {machine.storages.length > 0 ? <UnitIcon id="storage" type="storage" /> : undefined}
- {hasNoUnits ? <Badge color="warning">Machine with no units</Badge> : undefined}
- </div>
- </ListGroupItem>
+ <Flex onClick={() => onClick()}>
+ {machine.cpus.length > 0 ? <UnitIcon id="cpu" type="CPU" /> : undefined}
+ {machine.gpus.length > 0 ? <UnitIcon id="gpu" type="GPU" /> : undefined}
+ {machine.memories.length > 0 ? <UnitIcon id="memory" type="memory" /> : undefined}
+ {machine.storages.length > 0 ? <UnitIcon id="storage" type="storage" /> : undefined}
+ {hasNoUnits ? (
+ <Label variant="outline" color="orange">
+ Machine with no units
+ </Label>
+ ) : undefined}
+ </Flex>
)
}
MachineComponent.propTypes = {
- machine: Machine,
- position: PropTypes.number,
+ machine: Machine.isRequired,
onClick: PropTypes.func,
}
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
index e024a417..27834cf4 100644
--- 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
@@ -1,21 +1,66 @@
import PropTypes from 'prop-types'
import React from 'react'
-import { machineList } from './MachineListComponent.module.scss'
import MachineComponent from './MachineComponent'
import { Machine } from '../../../../../shapes'
-import EmptySlotComponent from './EmptySlotComponent'
+import {
+ Badge,
+ Button,
+ DataList,
+ DataListAction,
+ DataListCell,
+ DataListItem,
+ DataListItemCells,
+ DataListItemRow,
+} from '@patternfly/react-core'
+import { AngleRightIcon, PlusIcon } from '@patternfly/react-icons'
-const MachineListComponent = ({ machines = [], onSelect, onAdd }) => {
+function MachineListComponent({ machines = [], onSelect, onAdd }) {
return (
- <ul className={`list-group ${machineList}`}>
- {machines.map((machine, index) => {
- if (machine === null) {
- return <EmptySlotComponent key={index} onAdd={() => onAdd(index + 1)} />
- } else {
- return <MachineComponent key={index} onClick={() => onSelect(index + 1)} machine={machine} />
- }
- })}
- </ul>
+ <DataList aria-label="Rack Units">
+ {machines.map((machine, index) =>
+ machine ? (
+ <DataListItem key={index} onClick={() => onSelect(index + 1)}>
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell isIcon key="icon">
+ <Badge isRead>{machines.length - index}U</Badge>
+ </DataListCell>,
+ <DataListCell key="primary content">
+ <MachineComponent onClick={() => onSelect(index + 1)} machine={machine} />
+ </DataListCell>,
+ ]}
+ />
+ <DataListAction id="goto" aria-label="Goto Machine" aria-labelledby="goto">
+ <Button isSmall variant="plain" className="pf-u-p-0">
+ <AngleRightIcon />
+ </Button>
+ </DataListAction>
+ </DataListItemRow>
+ </DataListItem>
+ ) : (
+ <DataListItem key={index}>
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell isIcon key="icon">
+ <Badge isRead>{machines.length - index}U</Badge>
+ </DataListCell>,
+ <DataListCell key="add" className="text-secondary">
+ Empty Slot
+ </DataListCell>,
+ ]}
+ />
+ <DataListAction id="add" aria-label="Add Machine" aria-labelledby="add">
+ <Button isSmall variant="plain" className="pf-u-p-0" onClick={() => onAdd(index + 1)}>
+ <PlusIcon />
+ </Button>
+ </DataListAction>
+ </DataListItemRow>
+ </DataListItem>
+ )
+ )}
+ </DataList>
)
}
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.module.scss b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.module.scss
deleted file mode 100644
index f075aac9..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.module.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.machineList li {
- min-height: 64px;
-}
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListContainer.js
new file mode 100644
index 00000000..54e6db0a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListContainer.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React, { useMemo } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import MachineListComponent from './MachineListComponent'
+import { goFromRackToMachine } from '../../../../../redux/actions/interaction-level'
+import { addMachine } from '../../../../../redux/actions/topology/rack'
+
+function MachineListContainer({ tileId, ...props }) {
+ const rack = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack])
+ const machines = useSelector((state) => rack.machines.map((id) => state.objects.machine[id]))
+ const machinesNull = useMemo(() => {
+ const res = Array(rack.capacity).fill(null)
+ for (const machine of machines) {
+ res[machine.position - 1] = machine
+ }
+ return res
+ }, [rack, machines])
+ const dispatch = useDispatch()
+
+ return (
+ <MachineListComponent
+ {...props}
+ machines={machinesNull}
+ onAdd={(index) => dispatch(addMachine(index))}
+ onSelect={(index) => dispatch(goFromRackToMachine(index))}
+ />
+ )
+}
+
+MachineListContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default MachineListContainer
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameContainer.js
new file mode 100644
index 00000000..11529b55
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameContainer.js
@@ -0,0 +1,22 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import NameComponent from '../NameComponent'
+import { editRackName } from '../../../../../redux/actions/topology/rack'
+
+const RackNameContainer = ({ tileId }) => {
+ const rackName = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack].name)
+ const dispatch = useDispatch()
+ const callback = (name) => {
+ if (name) {
+ dispatch(editRackName(name))
+ }
+ }
+ return <NameComponent name={rackName} onEdit={callback} />
+}
+
+RackNameContainer.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
+export default RackNameContainer
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
index 74313bf7..dd5117f7 100644
--- 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
@@ -1,25 +1,58 @@
+import PropTypes from 'prop-types'
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 { sidebarContainer, sidebarHeaderContainer, machineListContainer } from './RackSidebarComponent.module.scss'
-import AddPrefabContainer from '../../../../../containers/app/sidebars/topology/rack/AddPrefabContainer'
+import { machineListContainer, sidebarContainer } from './RackSidebarComponent.module.scss'
+import RackNameContainer from './RackNameContainer'
+import AddPrefabContainer from './AddPrefabContainer'
+import DeleteRackContainer from './DeleteRackContainer'
+import MachineListContainer from './MachineListContainer'
+import {
+ Skeleton,
+ TextContent,
+ TextList,
+ TextListItem,
+ TextListItemVariants,
+ TextListVariants,
+ Title,
+} from '@patternfly/react-core'
+import { useSelector } from 'react-redux'
+
+function RackSidebarComponent({ tileId }) {
+ const rack = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack])
-const RackSidebarComponent = () => {
return (
- <div className={`${sidebarContainer} flex-column`}>
- <div className={sidebarHeaderContainer}>
- <RackNameContainer />
- <BackToRoomContainer />
+ <div className={sidebarContainer}>
+ <TextContent>
+ <Title headingLevel="h2">Details</Title>
+ <TextList component={TextListVariants.dl}>
+ <TextListItem
+ component={TextListItemVariants.dt}
+ className="pf-u-display-inline-flex pf-u-align-items-center"
+ >
+ Name
+ </TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>
+ <RackNameContainer tileId={tileId} />
+ </TextListItem>
+ <TextListItem component={TextListItemVariants.dt}>Capacity</TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>
+ {rack?.capacity ?? <Skeleton screenreaderText="Loading rack" />}
+ </TextListItem>
+ </TextList>
+ <Title headingLevel="h2">Actions</Title>
<AddPrefabContainer />
<DeleteRackContainer />
- </div>
- <div className={`${machineListContainer} mt-2`}>
- <MachineListContainer />
+
+ <Title headingLevel="h2">Slots</Title>
+ </TextContent>
+ <div className={machineListContainer}>
+ <MachineListContainer tileId={tileId} />
</div>
</div>
)
}
+RackSidebarComponent.propTypes = {
+ tileId: PropTypes.string.isRequired,
+}
+
export default RackSidebarComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.module.scss b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.module.scss
index 8ce3836a..6f258aec 100644
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.module.scss
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.module.scss
@@ -2,13 +2,11 @@
display: flex;
height: 100%;
max-height: 100%;
-}
-
-.sidebarHeaderContainer {
- flex: 0;
+ flex-direction: column;
}
.machineListContainer {
flex: 1;
overflow-y: scroll;
+ margin-top: 10px;
}
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarContainer.js
new file mode 100644
index 00000000..2b31413d
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarContainer.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useSelector } from 'react-redux'
+import RackSidebarComponent from './RackSidebarComponent'
+
+const RackSidebarContainer = (props) => {
+ const tileId = useSelector((state) => state.interactionLevel.tileId)
+ return <RackSidebarComponent {...props} tileId={tileId} />
+}
+
+export default RackSidebarContainer
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
deleted file mode 100644
index 043cc713..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faAngleLeft } from '@fortawesome/free-solid-svg-icons'
-
-const BackToBuildingComponent = ({ onClick }) => (
- <div className="btn btn-secondary btn-block mb-2" onClick={onClick}>
- <FontAwesomeIcon icon={faAngleLeft} className="mr-2" />
- Back to building
- </div>
-)
-
-BackToBuildingComponent.propTypes = {
- onClick: PropTypes.func,
-}
-
-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
deleted file mode 100644
index d81bad0f..00000000
--- a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faTrash } from '@fortawesome/free-solid-svg-icons'
-import { Button } from 'reactstrap'
-
-const DeleteRoomComponent = ({ onClick }) => (
- <Button color="danger" outline block onClick={onClick}>
- <FontAwesomeIcon icon={faTrash} className="mr-2" />
- Delete this room
- </Button>
-)
-
-DeleteRoomComponent.propTypes = {
- onClick: PropTypes.func,
-}
-
-export default DeleteRoomComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomContainer.js
new file mode 100644
index 00000000..284c4d53
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomContainer.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { useState } from 'react'
+import { useDispatch } from 'react-redux'
+import ConfirmationModal from '../../../../../components/modals/ConfirmationModal'
+import { deleteRoom } from '../../../../../redux/actions/topology/room'
+import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon'
+import { Button } from '@patternfly/react-core'
+
+const DeleteRoomContainer = () => {
+ const dispatch = useDispatch()
+ const [isVisible, setVisible] = useState(false)
+ const callback = (isConfirmed) => {
+ if (isConfirmed) {
+ dispatch(deleteRoom())
+ }
+ setVisible(false)
+ }
+ return (
+ <>
+ <Button variant="danger" icon={<TrashIcon />} isBlock onClick={() => setVisible(true)}>
+ Delete this room
+ </Button>
+ <ConfirmationModal
+ title="Delete this room"
+ message="Are you sure you want to delete this room?"
+ isOpen={isVisible}
+ callback={callback}
+ />
+ </>
+ )
+}
+
+export default DeleteRoomContainer
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomContainer.js
new file mode 100644
index 00000000..6db2bfb6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomContainer.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { finishRoomEdit, startRoomEdit } from '../../../../../redux/actions/topology/building'
+import CheckIcon from '@patternfly/react-icons/dist/js/icons/check-icon'
+import PencilAltIcon from '@patternfly/react-icons/dist/js/icons/pencil-alt-icon'
+import { Button } from '@patternfly/react-core'
+
+const EditRoomContainer = () => {
+ const isEditing = useSelector((state) => state.construction.currentRoomInConstruction !== '-1')
+ const isInRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode)
+
+ const dispatch = useDispatch()
+ const onEdit = () => dispatch(startRoomEdit())
+ const onFinish = () => dispatch(finishRoomEdit())
+
+ return isEditing ? (
+ <Button variant="tertiary" icon={<CheckIcon />} isBlock onClick={onFinish} className="pf-u-mb-sm">
+ Finish editing room
+ </Button>
+ ) : (
+ <Button
+ variant="tertiary"
+ icon={<PencilAltIcon />}
+ isBlock
+ disabled={isInRackConstructionMode}
+ onClick={() => (isInRackConstructionMode ? undefined : onEdit())}
+ className="pf-u-mb-sm"
+ >
+ Edit the tiles of this room
+ </Button>
+ )
+}
+
+export default EditRoomContainer
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
index 0a27910c..8aebe969 100644
--- 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
@@ -1,14 +1,13 @@
import PropTypes from 'prop-types'
import React from 'react'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faTimes, faPlus } from '@fortawesome/free-solid-svg-icons'
-import { Button } from 'reactstrap'
+import { Button } from '@patternfly/react-core'
+import PlusIcon from '@patternfly/react-icons/dist/js/icons/plus-icon'
+import TimesIcon from '@patternfly/react-icons/dist/js/icons/times-icon'
const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, isEditingRoom }) => {
if (inRackConstructionMode) {
return (
- <Button color="primary" block onClick={onStop}>
- <FontAwesomeIcon icon={faTimes} className="mr-2" />
+ <Button isBlock={true} icon={<TimesIcon />} onClick={onStop}>
Stop rack construction
</Button>
)
@@ -16,13 +15,12 @@ const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, is
return (
<Button
- color="primary"
- outline
- block
- disabled={isEditingRoom}
+ icon={<PlusIcon />}
+ isBlock
+ isDisabled={isEditingRoom}
onClick={() => (isEditingRoom ? undefined : onStart())}
+ className="pf-u-mb-sm"
>
- <FontAwesomeIcon icon={faPlus} className="mr-2" />
Start rack construction
</Button>
)
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionContainer.js
new file mode 100644
index 00000000..38af447a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionContainer.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { startRackConstruction, stopRackConstruction } from '../../../../../redux/actions/topology/room'
+import RackConstructionComponent from './RackConstructionComponent'
+
+const RackConstructionContainer = (props) => {
+ const isRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode)
+ const isEditingRoom = useSelector((state) => state.construction.currentRoomInConstruction !== '-1')
+
+ const dispatch = useDispatch()
+ const onStart = () => dispatch(startRackConstruction())
+ const onStop = () => dispatch(stopRackConstruction())
+ return (
+ <RackConstructionComponent
+ {...props}
+ inRackConstructionMode={isRackConstructionMode}
+ isEditingRoom={isEditingRoom}
+ onStart={onStart}
+ onStop={onStop}
+ />
+ )
+}
+
+export default RackConstructionContainer
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomName.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomName.js
new file mode 100644
index 00000000..d7b006a6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomName.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import NameComponent from '../../../../../components/app/sidebars/topology/NameComponent'
+import { editRoomName } from '../../../../../redux/actions/topology/room'
+
+function RoomName({ roomId }) {
+ const roomName = useSelector((state) => state.objects.room[roomId].name)
+ const dispatch = useDispatch()
+ const callback = (name) => {
+ if (name) {
+ dispatch(editRoomName(name))
+ }
+ }
+ return <NameComponent name={roomName} onEdit={callback} />
+}
+
+RoomName.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
+export default RoomName
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
index 1bc6533e..fac58c51 100644
--- 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
@@ -1,20 +1,43 @@
+import PropTypes from 'prop-types'
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'
+import RoomName from './RoomName'
+import RackConstructionContainer from './RackConstructionContainer'
+import EditRoomContainer from './EditRoomContainer'
+import DeleteRoomContainer from './DeleteRoomContainer'
+import {
+ TextContent,
+ TextList,
+ TextListItem,
+ TextListItemVariants,
+ TextListVariants,
+ Title,
+} from '@patternfly/react-core'
-const RoomSidebarComponent = () => {
+const RoomSidebarComponent = ({ roomId }) => {
return (
- <div>
- <RoomNameContainer />
- <BackToBuildingContainer />
+ <TextContent>
+ <Title headingLevel="h2">Details</Title>
+ <TextList component={TextListVariants.dl}>
+ <TextListItem
+ component={TextListItemVariants.dt}
+ className="pf-u-display-inline-flex pf-u-align-items-center"
+ >
+ Name
+ </TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>
+ <RoomName roomId={roomId} />
+ </TextListItem>
+ </TextList>
+ <Title headingLevel="h2">Construction</Title>
<RackConstructionContainer />
<EditRoomContainer />
<DeleteRoomContainer />
- </div>
+ </TextContent>
)
}
+RoomSidebarComponent.propTypes = {
+ roomId: PropTypes.string.isRequired,
+}
+
export default RoomSidebarComponent
diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarContainer.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarContainer.js
new file mode 100644
index 00000000..2076b00e
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarContainer.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { useSelector } from 'react-redux'
+import RoomSidebarComponent from './RoomSidebarComponent'
+
+const RoomSidebarContainer = (props) => {
+ const roomId = useSelector((state) => state.interactionLevel.roomId)
+ return <RoomSidebarComponent {...props} roomId={roomId} />
+}
+
+export default RoomSidebarContainer
diff --git a/opendc-web/opendc-web-ui/src/components/home/ContactSection.js b/opendc-web/opendc-web-ui/src/components/home/ContactSection.js
deleted file mode 100644
index 60a7e6a3..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/ContactSection.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import React from 'react'
-import Image from 'next/image'
-import { Row, Col } from 'reactstrap'
-import ContentSection from './ContentSection'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faExclamationTriangle, faEnvelope } from '@fortawesome/free-solid-svg-icons'
-import { faGithub } from '@fortawesome/free-brands-svg-icons'
-import { contactSection, tudelftIcon } from './ContactSection.module.scss'
-
-const ContactSection = () => (
- <ContentSection name="contact" title="Contact" className={contactSection}>
- <Row className="justify-content-center">
- <Col md="4">
- <a href="https://github.com/atlarge-research/opendc">
- <FontAwesomeIcon icon={faGithub} size="3x" className="mb-2" />
- <div className="w-100" />
- atlarge-research/opendc
- </a>
- </Col>
- <Col md="4">
- <a href="mailto:opendc@atlarge-research.com">
- <FontAwesomeIcon icon={faEnvelope} size="3x" className="mb-2" />
- <div className="w-100" />
- opendc@atlarge-research.com
- </a>
- </Col>
- </Row>
- <Row>
- <Col className="text-center">
- <Image
- src="/img/tudelft-icon.png"
- className={tudelftIcon}
- layout="intrinsic"
- width={162}
- height={100}
- alt="TU Delft"
- />
- </Col>
- </Row>
- <Row>
- <Col className="text-center">
- A project by the &nbsp;
- <a href="http://atlarge.science" target="_blank" rel="noopener noreferrer">
- <strong>@Large Research Group</strong>
- </a>
- .
- </Col>
- </Row>
- <Row>
- <Col className="text-center disclaimer mt-5 small">
- <FontAwesomeIcon icon={faExclamationTriangle} size="2x" className="mr-2" />
- <br />
- <strong>Disclaimer: </strong>
- 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.txt">license</a>
- </strong>
- ). Sorry for the inconvenience.
- </Col>
- </Row>
- </ContentSection>
-)
-
-export default ContactSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/ContactSection.module.scss b/opendc-web/opendc-web-ui/src/components/home/ContactSection.module.scss
deleted file mode 100644
index 9ab4fcb1..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/ContactSection.module.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-.contactSection {
- background-color: #444;
- color: #ddd;
-
- a {
- color: #ddd;
-
- &:hover {
- color: #fff;
- }
- }
-
- .tudelftIcon {
- 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
deleted file mode 100644
index abaa565c..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/ContentSection.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import classNames from 'classnames'
-import { Container } from 'reactstrap'
-import { contentSection } from './ContentSection.module.scss'
-
-const ContentSection = ({ name, title, children, className }) => (
- <section id={name} className={classNames(className, contentSection)}>
- <Container>
- <h1>{title}</h1>
- {children}
- </Container>
- </section>
-)
-
-ContentSection.propTypes = {
- name: PropTypes.string.isRequired,
- title: PropTypes.string.isRequired,
- children: PropTypes.node,
- className: PropTypes.string,
-}
-
-export default ContentSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/ContentSection.module.scss b/opendc-web/opendc-web-ui/src/components/home/ContentSection.module.scss
deleted file mode 100644
index d27a0ce0..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/ContentSection.module.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-@import 'src/style/_variables.scss';
-
-.contentSection {
- padding-top: 50px;
- padding-bottom: 50px;
- 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
deleted file mode 100644
index f9000d32..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/IntroSection.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import Image from 'next/image'
-import { Container, Row, Col } from 'reactstrap'
-
-const IntroSection = ({ className }) => (
- <section id="intro" className={className}>
- <Container className="pt-5 pb-3">
- <Row className="justify-content-center">
- <Col xl="4" lg="4" md="4" sm="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>
- </Col>
- <Col xl="4" lg="4" md="4" sm="8">
- <Image
- src="/img/datacenter-drawing.png"
- className="col-12"
- layout="intrinsic"
- width={350}
- height={197}
- 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>
- </Col>
- <Col xl="4" lg="4" md="4" sm="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>
- </Col>
- </Row>
- </Container>
- </section>
-)
-
-IntroSection.propTypes = {
- className: PropTypes.string,
-}
-
-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
deleted file mode 100644
index 98aa0af2..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react'
-import Image from 'next/image'
-import { Container, Jumbotron, Button } from 'reactstrap'
-import { jumbotronHeader, jumbotron, dc } from './JumbotronHeader.module.scss'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'
-
-const JumbotronHeader = () => (
- <section className={jumbotronHeader}>
- <Container>
- <Jumbotron className={jumbotron}>
- <h1>
- Open<span className={dc}>DC</span>
- </h1>
- <p className="lead">Collaborative Datacenter Simulation and Exploration for Everybody</p>
- <div className="mt-5">
- <Image src="/img/logo.png" layout="intrinsic" height={110} width={110} alt="OpenDC" />
- </div>
- <p className="lead mt-5">
- <Button
- tag="a"
- target="_blank"
- href="https://atlarge-research.com/pdfs/ccgrid21-opendc-paper.pdf"
- color="warning"
- >
- Read about <strong>OpenDC 2.0</strong> <FontAwesomeIcon icon={faExternalLinkAlt} />
- </Button>
- </p>
- </Jumbotron>
- </Container>
- </section>
-)
-
-export default JumbotronHeader
diff --git a/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.module.scss b/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.module.scss
deleted file mode 100644
index 567b3e73..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.module.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-.jumbotronHeader {
- background: #00a6d6;
-}
-
-.jumbotron {
- background-color: inherit;
- margin-bottom: 0;
- text-align: center;
-
- padding-top: 120px;
- padding-bottom: 120px;
-
- img {
- max-width: 110px;
- }
-
- h1 {
- color: #fff;
- font-size: 4.5em;
-
- .dc {
- color: #fff;
- font-weight: bold;
- }
- }
-
- :global(.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
deleted file mode 100644
index 8959663a..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/ModelingSection.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import ScreenshotSection from './ScreenshotSection'
-
-const ModelingSection = ({ className }) => (
- <ScreenshotSection
- name="modeling"
- title="Datacenter Modeling"
- imageUrl="/img/screenshot-construction.png"
- caption="Building a datacenter in OpenDC"
- imageIsRight={true}
- className={className}
- >
- <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>
-)
-
-ModelingSection.propTypes = {
- className: PropTypes.string,
-}
-
-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
deleted file mode 100644
index 58fe1710..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import Image from 'next/image'
-import { Row, Col } from 'reactstrap'
-import ContentSection from './ContentSection'
-import { screenshot } from './ScreenshotSection.module.scss'
-
-const ScreenshotSection = ({ className, name, title, imageUrl, caption, imageIsRight, children }) => (
- <ContentSection name={name} title={title} className={className}>
- <Row>
- <Col xl="5" lg="5" md="5" sm="12" className={`text-left my-auto ${!imageIsRight ? 'order-1' : ''}`}>
- {children}
- </Col>
- <Col xl="7" lg="7" md="7" sm="12">
- <Image
- src={imageUrl}
- className={`col-12 ${screenshot}`}
- layout="intrinsic"
- width={635}
- height={419}
- alt={caption}
- />
- <div className="row text-muted justify-content-center">{caption}</div>
- </Col>
- </Row>
- </ContentSection>
-)
-
-ScreenshotSection.propTypes = {
- className: PropTypes.string,
- name: PropTypes.string,
- title: PropTypes.string,
- imageUrl: PropTypes.string,
- caption: PropTypes.string,
- imageIsRight: PropTypes.bool,
- children: PropTypes.node,
-}
-
-export default ScreenshotSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.module.scss b/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.module.scss
deleted file mode 100644
index 7e22de32..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.module.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.screenshot {
- 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
deleted file mode 100644
index 46ce6a35..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/SimulationSection.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import Image from 'next/image'
-import { Col, Row } from 'reactstrap'
-import ContentSection from './ContentSection'
-
-const SimulationSection = ({ className }) => {
- return (
- <ContentSection name="project" title="Datecenter Simulation" className={className}>
- <Row>
- <Col xl="5" lg="5" md="5" sm="2" className="text-left my-auto order-1">
- <h3>Working with OpenDC:</h3>
- <ul>
- <li>Seamlessly switch between construction and simulation modes</li>
- <li>
- Choose one of several predefined workloads (Business Critical, Workflows, Machine Learning,
- Serverless, etc.)
- </li>
- <li>Compare datacenter topologies using automated plots and visual summaries</li>
- </ul>
- </Col>
- <Col xl="7" lg="7" md="7" sm="12">
- <Image
- src="/img/screenshot-simulation.png"
- className="col-12"
- layout="intrinsic"
- width={635}
- height={419}
- alt="Running an experiment in OpenDC"
- />
- <Row className="text-muted justify-content-center">Running an experiment in OpenDC</Row>
- </Col>
- </Row>
- <Row className="mt-5">
- <Col xl="5" lg="5" md="5" sm="2" className="text-left my-auto">
- <h3>OpenDC&apos;s Simulator:</h3>
- <ul>
- <li>Includes a detailed operational model of modern datacenters</li>
- <li>
- Support for emerging datacenter technologies around <em>cloud computing</em>,{' '}
- <em>serverless computing</em>, <em>big data</em>, and <em>machine learning</em>
- </li>
- </ul>
- </Col>
-
- <Col xl="7" lg="7" md="7" sm="12">
- <Image
- src="/img/opendc-architecture.png"
- className="col-12"
- layout="intrinsic"
- width={635}
- height={232}
- alt="OpenDC's Architecture"
- />
- <Row className="text-muted justify-content-center">OpenDC&apos;s Architecture</Row>
- </Col>
- </Row>
- </ContentSection>
- )
-}
-
-SimulationSection.propTypes = {
- className: PropTypes.string,
-}
-
-export default SimulationSection
diff --git a/opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js b/opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js
deleted file mode 100644
index aa689ba4..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { Row, Col } from 'reactstrap'
-import ContentSection from './ContentSection'
-
-const Stakeholder = ({ name, title, subtitle }) => (
- <Col xl="4" lg="4" md="4" sm="6">
- <Col
- tag="img"
- xl="3"
- lg="3"
- md="4"
- sm="4"
- src={'img/stakeholders/' + name + '.png'}
- className="img-fluid"
- alt={title}
- />
- <div className="text-center mt-2">
- <h4>{title}</h4>
- <p>{subtitle}</p>
- </div>
- </Col>
-)
-
-Stakeholder.propTypes = {
- name: PropTypes.string,
- title: PropTypes.string,
- subtitle: PropTypes.string,
-}
-
-const StakeholderSection = ({ className }) => (
- <ContentSection name="stakeholders" title="Stakeholders" className={className}>
- <Row className="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" />
- </Row>
- </ContentSection>
-)
-
-StakeholderSection.propTypes = {
- className: PropTypes.string,
-}
-
-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
deleted file mode 100644
index dd057a93..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/TeamSection.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import Image from 'next/image'
-import { Row, Col } from 'reactstrap'
-import ContentSection from './ContentSection'
-
-const TeamLead = ({ photoId, name, description }) => (
- <Col xl="3" lg="3" md="4" sm="6" className="justify-content-center">
- <Row>
- <Col xl="10" lg="10" md="10" sm="8" col="5" className="my-2 mx-auto" alt={name}>
- <Image
- src={'/img/portraits/' + photoId + '.png'}
- layout="intrinsic"
- width={182}
- height={182}
- alt={name}
- />
- </Col>
- <Col>
- <h4>{name}</h4>
- <div className="team-member-description">{description}</div>
- </Col>
- </Row>
- </Col>
-)
-
-TeamLead.propTypes = {
- photoId: PropTypes.string,
- name: PropTypes.string,
- description: PropTypes.string,
-}
-
-const TeamMember = ({ photoId, name }) => (
- <Col xl="2" lg="2" md="3" sm="4" className="justify-content-center">
- <Row>
- <Col xl="10" lg="10" md="10" sm="8" xs="5" className="my-2 mx-auto">
- <Image
- src={'/img/portraits/' + photoId + '.png'}
- layout="intrinsic"
- width={100}
- height={100}
- alt={name}
- />
- </Col>
- <Col>
- <h5>{name}</h5>
- </Col>
- </Row>
- </Col>
-)
-
-TeamMember.propTypes = {
- photoId: PropTypes.string,
- name: PropTypes.string,
-}
-
-const TeamSection = ({ className }) => (
- <ContentSection name="team" title="OpenDC Team" className={className}>
- <Row className="justify-content-center">
- <TeamLead photoId="aiosup" name="Prof. dr. ir. Alexandru Iosup" description="Project Lead" />
- <TeamLead photoId="fmastenbroek" name="Fabian Mastenbroek" description="Technology Lead" />
- <TeamLead photoId="gandreadis" name="Georgios Andreadis" description="Former Technology Lead (2018-2020)" />
- <TeamLead photoId="vvanbeek" name="Vincent van Beek" description="Former Technology Lead (2017-2018)" />
- </Row>
- <Row className="justify-content-center mt-5">
- <TeamMember photoId="loverweel" name="Leon Overweel" />
- <TeamMember photoId="lfdversluis" name="Laurens Versluis" />
- <TeamMember photoId="evaneyk" name="Erwin van Eyk" />
- <TeamMember photoId="sjounaid" name="Soufiane Jounaid" />
- <TeamMember photoId="wlai" name="Wenchen Lai" />
- <TeamMember photoId="hhe" name="Hongyu He" />
- <TeamMember photoId="jburley" name="Jacob Burley" />
- <TeamMember photoId="jbosch" name="Jaro Bosch" />
- </Row>
- </ContentSection>
-)
-
-TeamSection.propTypes = {
- className: PropTypes.string,
-}
-
-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
deleted file mode 100644
index e5131c2a..00000000
--- a/opendc-web/opendc-web-ui/src/components/home/TechnologiesSection.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { ListGroup, ListGroupItem } from 'reactstrap'
-import ContentSection from './ContentSection'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faWindowMaximize, faTv, faDatabase, faCogs } from '@fortawesome/free-solid-svg-icons'
-
-const TechnologiesSection = ({ className }) => (
- <ContentSection name="technologies" title="Technologies" className={className}>
- <ListGroup className="list-group text-left">
- <ListGroupItem color="primary" className="d-flex justify-content-between align-items-center">
- <span style={{ minWidth: 100 }}>
- <FontAwesomeIcon icon={faWindowMaximize} className="mr-2" />
- <strong className="">Browser</strong>
- </span>
- <span className="text-right">JavaScript, React, Redux, Konva</span>
- </ListGroupItem>
- <ListGroupItem color="warning" className="d-flex justify-content-between align-items-center">
- <span style={{ minWidth: 100 }}>
- <FontAwesomeIcon icon={faTv} className="mr-2" />
- <strong>Server</strong>
- </span>
- <span className="text-right">Python, Flask, FlaskSocketIO, OpenAPI</span>
- </ListGroupItem>
- <ListGroupItem color="success" className="d-flex justify-content-between align-items-center">
- <span style={{ minWidth: 100 }}>
- <FontAwesomeIcon icon={faDatabase} className="mr-2" />
- <strong>Database</strong>
- </span>
- <span className="text-right">MongoDB</span>
- </ListGroupItem>
- <ListGroupItem color="danger" className="d-flex justify-content-between align-items-center">
- <span style={{ minWidth: 100 }}>
- <FontAwesomeIcon icon={faCogs} className="mr-2" />
- <strong>Simulator</strong>
- </span>
- <span className="text-right">Kotlin</span>
- </ListGroupItem>
- </ListGroup>
- </ContentSection>
-)
-
-TechnologiesSection.propTypes = {
- className: PropTypes.string,
-}
-
-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
index 5a95810a..f6e1c98b 100644
--- a/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js
+++ b/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js
@@ -2,11 +2,11 @@ import PropTypes from 'prop-types'
import React from 'react'
import Modal from './Modal'
-function ConfirmationModal({ title, message, show, callback }) {
+function ConfirmationModal({ title, message, isOpen, callback }) {
return (
<Modal
title={title}
- show={show}
+ isOpen={isOpen}
onSubmit={() => callback(true)}
onCancel={() => callback(false)}
submitButtonType="danger"
@@ -20,7 +20,7 @@ function ConfirmationModal({ title, message, show, callback }) {
ConfirmationModal.propTypes = {
title: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
- show: PropTypes.bool.isRequired,
+ isOpen: PropTypes.bool.isRequired,
callback: PropTypes.func.isRequired,
}
diff --git a/opendc-web/opendc-web-ui/src/components/modals/Modal.js b/opendc-web/opendc-web-ui/src/components/modals/Modal.js
index 8ab3924c..d4577062 100644
--- a/opendc-web/opendc-web-ui/src/components/modals/Modal.js
+++ b/opendc-web/opendc-web-ui/src/components/modals/Modal.js
@@ -1,43 +1,27 @@
-import React, { useState, useEffect } from 'react'
+import React from 'react'
import PropTypes from 'prop-types'
-import { Modal as RModal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'
+import { Button, Modal as PModal, ModalVariant } from '@patternfly/react-core'
-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()
- }
- }
+function Modal({ children, title, isOpen, onSubmit, onCancel, submitButtonType, submitButtonText }) {
+ const actions = [
+ <Button variant={submitButtonType} onClick={onSubmit} key="confirm">
+ {submitButtonText}
+ </Button>,
+ <Button variant="link" onClick={onCancel} key="cancel">
+ Cancel
+ </Button>,
+ ]
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>
+ <PModal variant={ModalVariant.small} isOpen={isOpen} onClose={onCancel} title={title} actions={actions}>
+ {children}
+ </PModal>
)
}
Modal.propTypes = {
title: PropTypes.string.isRequired,
- show: PropTypes.bool.isRequired,
+ isOpen: PropTypes.bool,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
submitButtonType: PropTypes.string,
@@ -48,7 +32,7 @@ Modal.propTypes = {
Modal.defaultProps = {
submitButtonType: 'primary',
submitButtonText: 'Save',
- show: false,
+ isOpen: 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
index 6758fdc0..392a729e 100644
--- a/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js
+++ b/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js
@@ -1,31 +1,60 @@
import PropTypes from 'prop-types'
-import React, { useRef } from 'react'
+import React, { useRef, useState } from 'react'
import Modal from './Modal'
+import { Form, FormGroup, TextInput } from '@patternfly/react-core'
-function TextInputModal({ title, label, show, callback, initialValue }) {
+function TextInputModal({ title, label, isOpen, callback, initialValue }) {
const textInput = useRef(null)
- const onSubmit = () => {
- callback(textInput.current.value)
+ const [isSubmitted, setSubmitted] = useState(false)
+ const [isValid, setValid] = useState(true)
+
+ const resetState = () => {
textInput.current.value = ''
+ setSubmitted(false)
+ setValid(false)
+ }
+ const onSubmit = (event) => {
+ const value = textInput.current.value
+ setSubmitted(true)
+
+ if (event) {
+ event.preventDefault()
+ }
+
+ if (!value) {
+ setValid(false)
+ return false
+ }
+
+ callback(value)
+ resetState()
+ return true
}
const onCancel = () => {
callback(undefined)
- textInput.current.value = ''
+ resetState()
}
return (
- <Modal title={title} show={show} onSubmit={onSubmit} onCancel={onCancel}>
- <form
- onSubmit={(e) => {
- e.preventDefault()
- onSubmit()
- }}
- >
- <div className="form-group">
- <label className="form-control-label">{label}</label>
- <input type="text" className="form-control" ref={textInput} defaultValue={initialValue} />
- </div>
- </form>
+ <Modal title={title} isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form onSubmit={onSubmit}>
+ <FormGroup
+ label={label}
+ fieldId="text-input"
+ isRequired
+ validated={isSubmitted && !isValid ? 'error' : 'default'}
+ helperTextInvalid="This field cannot be empty"
+ >
+ <TextInput
+ id="text-input"
+ name="text-input"
+ isRequired
+ type="text"
+ ref={textInput}
+ defaultValue={initialValue}
+ />
+ </FormGroup>
+ </Form>
</Modal>
)
}
@@ -33,7 +62,7 @@ function TextInputModal({ title, label, show, callback, initialValue }) {
TextInputModal.propTypes = {
title: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
- show: PropTypes.bool.isRequired,
+ isOpen: PropTypes.bool.isRequired,
callback: PropTypes.func.isRequired,
initialValue: PropTypes.string,
}
diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModal.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModal.js
new file mode 100644
index 00000000..afe07597
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModal.js
@@ -0,0 +1,139 @@
+import PropTypes from 'prop-types'
+import React, { useRef, useState } from 'react'
+import Modal from '../Modal'
+import {
+ Form,
+ FormGroup,
+ FormSection,
+ NumberInput,
+ Select,
+ SelectGroup,
+ SelectOption,
+ SelectVariant,
+ TextInput,
+} from '@patternfly/react-core'
+import { METRIC_GROUPS, METRIC_NAMES } from '../../../util/available-metrics'
+
+const NewPortfolioModal = ({ isOpen, onSubmit: onSubmitUpstream, onCancel: onUpstreamCancel }) => {
+ const nameInput = useRef(null)
+ const [repeats, setRepeats] = useState(1)
+ const [isSelectOpen, setSelectOpen] = useState(false)
+ const [selectedMetrics, setSelectedMetrics] = useState([])
+
+ const [isSubmitted, setSubmitted] = useState(false)
+ const [errors, setErrors] = useState({})
+
+ const clearState = () => {
+ setSubmitted(false)
+ setErrors({})
+ nameInput.current.value = ''
+ setRepeats(1)
+ setSelectOpen(false)
+ setSelectedMetrics([])
+ }
+
+ const onSubmit = (event) => {
+ setSubmitted(true)
+
+ if (event) {
+ event.preventDefault()
+ }
+
+ const name = nameInput.current.value
+
+ if (!name) {
+ setErrors({ name: true })
+ return false
+ } else {
+ onSubmitUpstream(name, { enabledMetrics: selectedMetrics, repeatsPerScenario: repeats })
+ }
+
+ clearState()
+ return false
+ }
+ const onCancel = () => {
+ onUpstreamCancel()
+ clearState()
+ }
+
+ const onSelect = (event, selection) => {
+ if (selectedMetrics.includes(selection)) {
+ setSelectedMetrics((metrics) => metrics.filter((item) => item !== selection))
+ } else {
+ setSelectedMetrics((metrics) => [...metrics, selection])
+ }
+ }
+
+ return (
+ <Modal title="New Portfolio" isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form onSubmit={onSubmit}>
+ <FormSection>
+ <FormGroup
+ label="Name"
+ fieldId="name"
+ isRequired
+ validated={isSubmitted && errors.name ? 'error' : 'default'}
+ helperTextInvalid="This field cannot be empty"
+ >
+ <TextInput
+ name="name"
+ id="name"
+ type="text"
+ isRequired
+ ref={nameInput}
+ placeholder="My Portfolio"
+ />
+ </FormGroup>
+ </FormSection>
+ <FormSection title="Targets" titleElement="h4">
+ <FormGroup label="Metrics" fieldId="metrics">
+ <Select
+ variant={SelectVariant.typeaheadMulti}
+ typeAheadAriaLabel="Select a metric"
+ onToggle={() => setSelectOpen(!isSelectOpen)}
+ onSelect={onSelect}
+ onClear={() => setSelectedMetrics([])}
+ selections={selectedMetrics}
+ isOpen={isSelectOpen}
+ placeholderText="Select a metric"
+ menuAppendTo="parent"
+ maxHeight="300px"
+ chipGroupProps={{ numChips: 1 }}
+ isGrouped
+ >
+ {Object.entries(METRIC_GROUPS).map(([group, metrics]) => (
+ <SelectGroup label={group} key={group}>
+ {metrics.map((metric) => (
+ <SelectOption key={metric} value={metric}>
+ {METRIC_NAMES[metric]}
+ </SelectOption>
+ ))}
+ </SelectGroup>
+ ))}
+ </Select>
+ </FormGroup>
+ <FormGroup label="Repeats per Scenario" fieldId="repeats">
+ <NumberInput
+ id="repeats"
+ inputName="repeats"
+ type="number"
+ value={repeats}
+ onChange={(e) => setRepeats(Number(e.target.value))}
+ onPlus={() => setRepeats((r) => r + 1)}
+ onMinus={() => setRepeats((r) => r - 1)}
+ min={1}
+ />
+ </FormGroup>
+ </FormSection>
+ </Form>
+ </Modal>
+ )
+}
+
+NewPortfolioModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+}
+
+export default NewPortfolioModal
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
deleted file mode 100644
index 3c6b8724..00000000
--- a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js
+++ /dev/null
@@ -1,78 +0,0 @@
-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/NewScenarioModal.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModal.js
new file mode 100644
index 00000000..94d0d424
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModal.js
@@ -0,0 +1,159 @@
+import PropTypes from 'prop-types'
+import React, { useRef, useState } from 'react'
+import { Portfolio } from '../../../shapes'
+import Modal from '../Modal'
+import {
+ Checkbox,
+ Form,
+ FormGroup,
+ FormSection,
+ FormSelect,
+ FormSelectOption,
+ NumberInput,
+ TextInput,
+} from '@patternfly/react-core'
+import { useSchedulers, useTraces } from '../../../data/experiments'
+import { useProjectTopologies } from '../../../data/topology'
+import { usePortfolio } from '../../../data/project'
+
+const NewScenarioModal = ({ portfolioId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) => {
+ const { data: portfolio } = usePortfolio(portfolioId)
+ const { data: topologies = [] } = useProjectTopologies(portfolio?.projectId)
+ const { data: traces = [] } = useTraces()
+ const { data: schedulers = [] } = useSchedulers()
+
+ const [isSubmitted, setSubmitted] = useState(false)
+ const [traceLoad, setTraceLoad] = useState(100)
+ const [trace, setTrace] = useState(undefined)
+ const [topology, setTopology] = useState(undefined)
+ const [scheduler, setScheduler] = useState(undefined)
+ const [failuresEnabled, setFailuresEnabled] = useState(false)
+ const [opPhenEnabled, setOpPhenEnabled] = useState(false)
+ const nameInput = useRef(null)
+
+ const resetState = () => {
+ setSubmitted(false)
+ setTraceLoad(100)
+ setTrace(undefined)
+ setTopology(undefined)
+ setScheduler(undefined)
+ setFailuresEnabled(false)
+ setOpPhenEnabled(false)
+ nameInput.current.value = ''
+ }
+
+ const onSubmit = (event) => {
+ setSubmitted(true)
+
+ if (event) {
+ event.preventDefault()
+ }
+
+ const name = nameInput.current.value
+
+ onSubmitUpstream(
+ name,
+ portfolio._id,
+ {
+ traceId: trace || traces[0]._id,
+ loadSamplingFraction: traceLoad / 100,
+ },
+ {
+ topologyId: topology || topologies[0]._id,
+ },
+ {
+ failuresEnabled,
+ performanceInterferenceEnabled: opPhenEnabled,
+ schedulerName: scheduler || schedulers[0].name,
+ }
+ )
+
+ resetState()
+ return true
+ }
+ const onCancel = () => {
+ onCancelUpstream()
+ resetState()
+ }
+
+ return (
+ <Modal title="New Scenario" isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form onSubmit={onSubmit}>
+ <FormGroup label="Name" fieldId="name" isRequired>
+ <TextInput
+ id="name"
+ name="name"
+ type="text"
+ isDisabled={portfolio?.scenarioIds?.length === 0}
+ defaultValue={portfolio?.scenarioIds?.length === 0 ? 'Base scenario' : ''}
+ ref={nameInput}
+ />
+ </FormGroup>
+ <FormSection title="Workload">
+ <FormGroup label="Trace" fieldId="trace" isRequired>
+ <FormSelect id="trace" name="trace" value={trace} onChange={setTrace}>
+ {traces.map((trace) => (
+ <FormSelectOption value={trace._id} key={trace._id} label={trace.name} />
+ ))}
+ </FormSelect>
+ </FormGroup>
+ <FormGroup label="Load Sampling Fraction" fieldId="trace-load" isRequired>
+ <NumberInput
+ name="trace-load"
+ type="number"
+ min={0}
+ max={100}
+ value={traceLoad}
+ onMinus={() => setTraceLoad((load) => load - 1)}
+ onPlus={() => setTraceLoad((load) => load + 1)}
+ onChange={(e) => setTraceLoad(Number(e.target.value))}
+ unit="%"
+ />
+ </FormGroup>
+ </FormSection>
+ <FormSection title="Topology">
+ <FormGroup label="Topology" fieldId="topology" isRequired>
+ <FormSelect id="topology" name="topology" value={topology} onChange={setTopology}>
+ {topologies.map((topology) => (
+ <FormSelectOption value={topology._id} key={topology._id} label={topology.name} />
+ ))}
+ </FormSelect>
+ </FormGroup>
+
+ <FormGroup label="Scheduler" fieldId="scheduler" isRequired>
+ <FormSelect id="scheduler" name="scheduler" value={scheduler} onChange={setScheduler}>
+ {schedulers.map((scheduler) => (
+ <FormSelectOption value={scheduler.name} key={scheduler.name} label={scheduler.name} />
+ ))}
+ </FormSelect>
+ </FormGroup>
+ </FormSection>
+ <FormSection title="Operational Phenomena">
+ <Checkbox
+ label="Failures"
+ id="failures"
+ name="failures"
+ isChecked={failuresEnabled}
+ onChange={() => setFailuresEnabled((e) => !e)}
+ />
+ <Checkbox
+ label="Performance Interference"
+ id="perf-interference"
+ name="perf-interference"
+ isChecked={opPhenEnabled}
+ onChange={() => setOpPhenEnabled((e) => !e)}
+ />
+ </FormSection>
+ </Form>
+ </Modal>
+ )
+}
+
+NewScenarioModal.propTypes = {
+ portfolioId: PropTypes.string,
+ isOpen: PropTypes.bool.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+}
+
+export default NewScenarioModal
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
deleted file mode 100644
index 782812ac..00000000
--- a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import PropTypes from 'prop-types'
-import React, { useRef } from 'react'
-import { Form, FormGroup, Input, Label } from 'reactstrap'
-import { Scheduler, Topology, Trace } 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(Trace),
- topologies: PropTypes.arrayOf(Topology),
- schedulers: PropTypes.arrayOf(Scheduler),
- callback: PropTypes.func.isRequired,
-}
-
-export default NewScenarioModalComponent
diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModal.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModal.js
new file mode 100644
index 00000000..49952aec
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModal.js
@@ -0,0 +1,81 @@
+import PropTypes from 'prop-types'
+import React, { useRef, useState } from 'react'
+import Modal from '../Modal'
+import { Form, FormGroup, FormSelect, FormSelectOption, TextInput } from '@patternfly/react-core'
+import { useProjectTopologies } from '../../../data/topology'
+
+const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCancel: onCancelUpstream }) => {
+ const nameInput = useRef(null)
+ const [isSubmitted, setSubmitted] = useState(false)
+ const [originTopology, setOriginTopology] = useState(-1)
+ const [errors, setErrors] = useState({})
+
+ const { data: topologies = [] } = useProjectTopologies(projectId)
+
+ const clearState = () => {
+ nameInput.current.value = ''
+ setSubmitted(false)
+ setOriginTopology(-1)
+ setErrors({})
+ }
+
+ const onSubmit = (event) => {
+ setSubmitted(true)
+
+ if (event) {
+ event.preventDefault()
+ }
+
+ const name = nameInput.current.value
+
+ if (!name) {
+ setErrors({ name: true })
+ return false
+ } else if (originTopology === -1) {
+ onSubmitUpstream(name)
+ } else {
+ onSubmitUpstream(name, originTopology)
+ }
+
+ clearState()
+ return true
+ }
+
+ const onCancel = () => {
+ onCancelUpstream()
+ clearState()
+ }
+
+ return (
+ <Modal title="New Topology" isOpen={isOpen} onSubmit={onSubmit} onCancel={onCancel}>
+ <Form onSubmit={onSubmit}>
+ <FormGroup
+ label="Name"
+ fieldId="name"
+ isRequired
+ validated={isSubmitted && errors.name ? 'error' : 'default'}
+ helperTextInvalid="This field cannot be empty"
+ >
+ <TextInput id="name" name="name" type="text" isRequired ref={nameInput} />
+ </FormGroup>
+ <FormGroup label="Topology to duplicate" fieldId="origin" isRequired>
+ <FormSelect id="origin" name="origin" value={originTopology} onChange={setOriginTopology}>
+ <FormSelectOption value={-1} key={-1} label="None - start from scratch" />
+ {topologies.map((topology) => (
+ <FormSelectOption value={topology._id} key={topology._id} label={topology.name} />
+ ))}
+ </FormSelect>
+ </FormGroup>
+ </Form>
+ </Modal>
+ )
+}
+
+NewTopologyModal.propTypes = {
+ projectId: PropTypes.string,
+ isOpen: PropTypes.bool.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+}
+
+export default NewTopologyModal
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
deleted file mode 100644
index f06fe797..00000000
--- a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import PropTypes from 'prop-types'
-import { Form, FormGroup, Input, Label } from 'reactstrap'
-import React, { useRef } from 'react'
-import { Topology } 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(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
deleted file mode 100644
index 8aaaf847..00000000
--- a/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import Link from 'next/link'
-import { NavLink, NavItem as RNavItem } from 'reactstrap'
-import Navbar, { NavItem } from './Navbar'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faList } from '@fortawesome/free-solid-svg-icons'
-import { Project } from '../../shapes'
-
-const AppNavbarComponent = ({ project, fullWidth }) => (
- <Navbar fullWidth={fullWidth}>
- <NavItem route="/projects">
- <Link href="/projects" passHref>
- <NavLink title="My Projects">
- <FontAwesomeIcon icon={faList} className="mr-2" />
- My Projects
- </NavLink>
- </Link>
- </NavItem>
- {project ? (
- <RNavItem>
- <Link href={`/projects/${project._id}`} passHref>
- <NavLink title="Current Project">
- <span>{project.name}</span>
- </NavLink>
- </Link>
- </RNavItem>
- ) : undefined}
- </Navbar>
-)
-
-AppNavbarComponent.propTypes = {
- project: Project,
- fullWidth: PropTypes.bool,
-}
-
-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
deleted file mode 100644
index 57cb7a53..00000000
--- a/opendc-web/opendc-web-ui/src/components/navigation/HomeNavbar.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-import { NavItem, NavLink } from 'reactstrap'
-import Navbar from './Navbar'
-
-const HomeNavbar = () => (
- <Navbar fullWidth={false}>
- <NavLink href="#stakeholders">Stakeholders</NavLink>
- <NavLink href="#modeling">Modeling</NavLink>
- <NavLink href="#project">Project</NavLink>
- <NavLink href="#technologies">Technologies</NavLink>
- <NavLink href="#team">Team</NavLink>
- <NavLink href="#contact">Contact</NavLink>
- </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
deleted file mode 100644
index 4ab577e0..00000000
--- a/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { NavLink } from 'reactstrap'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
-
-const LogoutButton = ({ onLogout }) => (
- <NavLink className="logout" title="Sign out" onClick={onLogout}>
- <FontAwesomeIcon icon={faSignOutAlt} 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
deleted file mode 100644
index dc74bb8f..00000000
--- a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import PropTypes from 'prop-types'
-import React, { useState } from 'react'
-import Link from 'next/link'
-import { useRouter } from 'next/router'
-import Image from 'next/image'
-import {
- Navbar as RNavbar,
- NavItem as RNavItem,
- NavLink,
- NavbarBrand,
- NavbarToggler,
- Collapse,
- Nav,
- Container,
-} from 'reactstrap'
-import Login from '../../containers/auth/Login'
-import Logout from '../../containers/auth/Logout'
-import ProfileName from '../../containers/auth/ProfileName'
-import { login, navbar, opendcBrand } from './Navbar.module.scss'
-import { useAuth } from '../../auth'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faGithub } from '@fortawesome/free-brands-svg-icons'
-
-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 }}
- >
- <FontAwesomeIcon icon={faGithub} size="2x" />
- </a>
-)
-
-export const NavItem = ({ route, children }) => {
- const router = useRouter()
- const handleClick = (e) => {
- e.preventDefault()
- router.push(route)
- }
- return (
- <RNavItem onClick={handleClick} active={router.asPath === route}>
- {children}
- </RNavItem>
- )
-}
-
-NavItem.propTypes = {
- route: PropTypes.string.isRequired,
- children: PropTypes.node,
-}
-
-export const LoggedInSection = () => {
- const router = useRouter()
- const { isAuthenticated } = useAuth()
- return (
- <Nav navbar className="auth-links">
- {isAuthenticated ? (
- [
- router.asPath === '/' ? (
- <NavItem route="/projects" key="projects">
- <Link href="/projects" passHref>
- <NavLink title="My Projects" to="/projects">
- My Projects
- </NavLink>
- </Link>
- </NavItem>
- ) : (
- <RNavItem key="profile">
- <NavLink title="My Profile">
- <ProfileName />
- </NavLink>
- </RNavItem>
- ),
- <NavItem route="logout" key="logout">
- <Logout />
- </NavItem>,
- ]
- ) : (
- <RNavItem>
- <GitHubLink />
- <Login visible={true} className={login} />
- </RNavItem>
- )}
- </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" className={navbar}>
- <Container fluid={fullWidth}>
- <NavbarToggler onClick={toggle} />
- <NavbarBrand href="/" title="OpenDC" className={opendcBrand}>
- <div className="mb-n1">
- <Image src="/img/logo.png" layout="fixed" width={30} height={30} alt="OpenDC" />
- </div>
- </NavbarBrand>
-
- <Collapse isOpen={isOpen} navbar>
- <Nav className="mr-auto" navbar>
- {children}
- </Nav>
- <LoggedInSection />
- </Collapse>
- </Container>
- </RNavbar>
- )
-}
-
-Navbar.propTypes = {
- fullWidth: PropTypes.bool,
- children: PropTypes.node,
-}
-
-export default Navbar
diff --git a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.module.scss b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.module.scss
deleted file mode 100644
index 8b9e4c97..00000000
--- a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.module.scss
+++ /dev/null
@@ -1,36 +0,0 @@
-@import 'src/style/_mixins.scss';
-@import 'src/style/_variables.scss';
-
-.navbar {
- border-top: $blue 3px solid;
- border-bottom: $gray-semi-dark 1px solid;
- color: $gray-very-dark;
- background: #fafafb;
-}
-
-.opendcBrand {
- 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;
-
- @include 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
deleted file mode 100644
index 03a4894b..00000000
--- a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react'
-import { blinkingCursor } from './BlinkingCursor.module.scss'
-
-const BlinkingCursor = () => <span className={blinkingCursor}>_</span>
-
-export default BlinkingCursor
diff --git a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.module.scss b/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.module.scss
deleted file mode 100644
index aba0c604..00000000
--- a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.module.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.blinkingCursor {
- animation: blink 1s step-end infinite;
-}
-
-@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
deleted file mode 100644
index 6ded4350..00000000
--- a/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react'
-import { codeBlock } from './CodeBlock.module.scss'
-
-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={codeBlock} dangerouslySetInnerHTML={{ __html: textBlock }} />
-}
-
-export default CodeBlock
diff --git a/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.module.scss b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.module.scss
deleted file mode 100644
index 8af3ee6d..00000000
--- a/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.module.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.codeBlock {
- 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
deleted file mode 100644
index e6200b10..00000000
--- a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react'
-import Link from 'next/link'
-import BlinkingCursor from './BlinkingCursor'
-import CodeBlock from './CodeBlock'
-import { terminalWindow, terminalHeader, terminalBody, segfault, subTitle, homeBtn } from './TerminalWindow.module.scss'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faHome } from '@fortawesome/free-solid-svg-icons'
-
-const TerminalWindow = () => (
- <div className={terminalWindow}>
- <div className={terminalHeader}>Terminal -- bash</div>
- <div className={terminalBody}>
- <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={subTitle}>
- Got lost?
- <BlinkingCursor />
- </div>
- <Link href="/">
- <a className={homeBtn}>
- <FontAwesomeIcon icon={faHome} /> GET ME BACK TO OPENDC
- </a>
- </Link>
- </div>
- </div>
-)
-
-export default TerminalWindow
diff --git a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.module.scss b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.module.scss
deleted file mode 100644
index 614852d3..00000000
--- a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.module.scss
+++ /dev/null
@@ -1,61 +0,0 @@
-.terminalWindow {
- display: block;
- align-self: center;
-
- margin: auto;
-
- user-select: none;
- cursor: default;
-
- overflow: hidden;
-
- box-shadow: 5px 5px 20px #444444;
-}
-
-.terminalHeader {
- 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;
-}
-
-.terminalBody {
- font-family: monospace;
- text-align: center;
- background-color: #333333;
- color: #eeeeee;
- padding: 10px;
-
- height: 100%;
-}
-
-.segfault {
- text-align: left;
-}
-
-.subTitle {
- margin-top: 20px;
-}
-
-.homeBtn {
- margin-top: 10px;
- padding: 5px;
- display: inline-block;
- border: 1px solid #eeeeee;
- color: #eeeeee;
- text-decoration: none;
- cursor: pointer;
-
- transition: all 200ms;
-
- &:hover,
- &:active {
- background: #eeeeee;
- color: #333333;
- }
-}
diff --git a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js
index 5129c013..285217e9 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js
@@ -1,23 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
-import { Button, ButtonGroup } from 'reactstrap'
+import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'
import { filterPanel } from './FilterPanel.module.scss'
export const FILTERS = { SHOW_ALL: 'All Projects', SHOW_OWN: 'My Projects', SHOW_SHARED: 'Shared with me' }
const FilterPanel = ({ onSelect, activeFilter = 'SHOW_ALL' }) => (
- <ButtonGroup className={`${filterPanel} mb-2`}>
+ <ToggleGroup className={`${filterPanel} mb-2`}>
{Object.keys(FILTERS).map((filter) => (
- <Button
- color="secondary"
+ <ToggleGroupItem
key={filter}
- onClick={() => activeFilter === filter || onSelect(filter)}
- active={activeFilter === filter}
- >
- {FILTERS[filter]}
- </Button>
+ onChange={() => activeFilter === filter || onSelect(filter)}
+ isSelected={activeFilter === filter}
+ text={FILTERS[filter]}
+ />
))}
- </ButtonGroup>
+ </ToggleGroup>
)
FilterPanel.propTypes = {
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js
new file mode 100644
index 00000000..ae4cb9cd
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewPortfolio.js
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { PlusIcon } from '@patternfly/react-icons'
+import { Button } from '@patternfly/react-core'
+import { useState } from 'react'
+import NewPortfolioModal from '../modals/custom-components/NewPortfolioModal'
+import { useMutation } from 'react-query'
+
+function NewPortfolio({ projectId }) {
+ const [isVisible, setVisible] = useState(false)
+ const { mutate: addPortfolio } = useMutation('addPortfolio')
+
+ const onSubmit = (name, targets) => {
+ addPortfolio({ projectId, name, targets })
+ setVisible(false)
+ }
+
+ return (
+ <>
+ <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}>
+ New Portfolio
+ </Button>
+ <NewPortfolioModal isOpen={isVisible} onSubmit={onSubmit} onCancel={() => setVisible(false)} />
+ </>
+ )
+}
+
+NewPortfolio.propTypes = {
+ projectId: PropTypes.string,
+}
+
+export default NewPortfolio
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewProject.js b/opendc-web/opendc-web-ui/src/components/projects/NewProject.js
new file mode 100644
index 00000000..4f5d79cf
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewProject.js
@@ -0,0 +1,39 @@
+import React, { useState } from 'react'
+import TextInputModal from '../../components/modals/TextInputModal'
+import { Button } from '@patternfly/react-core'
+import { useMutation } from 'react-query'
+import { PlusIcon } from '@patternfly/react-icons'
+import { buttonContainer } from './NewProject.module.scss'
+
+/**
+ * A container for creating a new project.
+ */
+const NewProject = () => {
+ const [isVisible, setVisible] = useState(false)
+ const { mutate: addProject } = useMutation('addProject')
+
+ const onSubmit = (name) => {
+ if (name) {
+ addProject({ name })
+ }
+ setVisible(false)
+ }
+
+ return (
+ <>
+ <div className={buttonContainer}>
+ <Button
+ icon={<PlusIcon />}
+ color="primary"
+ className="pf-u-float-right"
+ onClick={() => setVisible(true)}
+ >
+ New Project
+ </Button>
+ </div>
+ <TextInputModal title="New Project" label="Project name" isOpen={isVisible} callback={onSubmit} />
+ </>
+ )
+}
+
+export default NewProject
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss b/opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss
new file mode 100644
index 00000000..5a0e74fc
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewProject.module.scss
@@ -0,0 +1,26 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+.buttonContainer {
+ flex: 0 1 auto;
+ padding: 20px 0;
+}
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewScenario.js b/opendc-web/opendc-web-ui/src/components/projects/NewScenario.js
new file mode 100644
index 00000000..6d4f48c1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewScenario.js
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { PlusIcon } from '@patternfly/react-icons'
+import { Button } from '@patternfly/react-core'
+import { useState } from 'react'
+import NewScenarioModal from '../modals/custom-components/NewScenarioModal'
+import { useMutation } from 'react-query'
+
+function NewScenario({ portfolioId }) {
+ const [isVisible, setVisible] = useState(false)
+ const { mutate: addScenario } = useMutation('addScenario')
+
+ const onSubmit = (name, portfolioId, trace, topology, operational) => {
+ addScenario({
+ portfolioId,
+ name,
+ trace,
+ topology,
+ operational,
+ })
+ setVisible(false)
+ }
+
+ return (
+ <>
+ <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}>
+ New Scenario
+ </Button>
+ <NewScenarioModal
+ portfolioId={portfolioId}
+ isOpen={isVisible}
+ onSubmit={onSubmit}
+ onCancel={() => setVisible(false)}
+ />
+ </>
+ )
+}
+
+NewScenario.propTypes = {
+ portfolioId: PropTypes.string,
+}
+
+export default NewScenario
diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js b/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js
new file mode 100644
index 00000000..c6c4171b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/NewTopology.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { PlusIcon } from '@patternfly/react-icons'
+import { Button } from '@patternfly/react-core'
+import { useState } from 'react'
+import NewTopologyModal from '../modals/custom-components/NewTopologyModal'
+import { useDispatch } from 'react-redux'
+import { addTopology } from '../../redux/actions/topologies'
+
+function NewTopology({ projectId }) {
+ const [isVisible, setVisible] = useState(false)
+ const dispatch = useDispatch()
+
+ const onSubmit = (name, duplicateId) => {
+ dispatch(addTopology(projectId, name, duplicateId))
+ setVisible(false)
+ }
+ return (
+ <>
+ <Button icon={<PlusIcon />} isSmall onClick={() => setVisible(true)}>
+ New Topology
+ </Button>
+ <NewTopologyModal
+ projectId={projectId}
+ isOpen={isVisible}
+ onSubmit={onSubmit}
+ onCancel={() => setVisible(false)}
+ />
+ </>
+ )
+}
+
+NewTopology.propTypes = {
+ projectId: PropTypes.string,
+}
+
+export default NewTopology
diff --git a/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js
new file mode 100644
index 00000000..45e399ed
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/PortfolioTable.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import Link from 'next/link'
+import { Table, TableBody, TableHeader } from '@patternfly/react-table'
+import React from 'react'
+import TableEmptyState from '../util/TableEmptyState'
+import { useProjectPortfolios } from '../../data/project'
+import { useMutation } from 'react-query'
+
+const PortfolioTable = ({ projectId }) => {
+ const { status, data: portfolios = [] } = useProjectPortfolios(projectId)
+ const { mutate: deletePortfolio } = useMutation('deletePortfolio')
+
+ const columns = ['Name', 'Scenarios', 'Metrics', 'Repeats']
+ const rows =
+ portfolios.length > 0
+ ? portfolios.map((portfolio) => [
+ {
+ title: (
+ <Link href={`/projects/${portfolio.projectId}/portfolios/${portfolio._id}`}>
+ {portfolio.name}
+ </Link>
+ ),
+ },
+
+ portfolio.scenarioIds.length === 1 ? '1 scenario' : `${portfolio.scenarioIds.length} scenarios`,
+
+ portfolio.targets.enabledMetrics.length === 1
+ ? '1 metric'
+ : `${portfolio.targets.enabledMetrics.length} metrics`,
+ portfolio.targets.repeatsPerScenario === 1
+ ? '1 repeat'
+ : `${portfolio.targets.repeatsPerScenario} repeats`,
+ ])
+ : [
+ {
+ heightAuto: true,
+ cells: [
+ {
+ props: { colSpan: 4 },
+ title: (
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading portfolios"
+ emptyTitle="No portfolios"
+ emptyText="You have not created any portfolio for this project yet. Click the New Portfolio button to create one."
+ />
+ ),
+ },
+ ],
+ },
+ ]
+
+ const actions =
+ portfolios.length > 0
+ ? [
+ {
+ title: 'Delete Portfolio',
+ onClick: (_, rowId) => deletePortfolio(portfolios[rowId]._id),
+ },
+ ]
+ : []
+
+ return (
+ <Table aria-label="Portfolio List" variant="compact" cells={columns} rows={rows} actions={actions}>
+ <TableHeader />
+ <TableBody />
+ </Table>
+ )
+}
+
+PortfolioTable.propTypes = {
+ projectId: PropTypes.string,
+}
+
+export default PortfolioTable
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js
deleted file mode 100644
index 4adf3205..00000000
--- a/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import Link from 'next/link'
-import { Button } from 'reactstrap'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPlay, faUsers, faTrash } from '@fortawesome/free-solid-svg-icons'
-
-const ProjectActionButtons = ({ projectId, onViewUsers, onDelete }) => (
- <td className="text-right">
- <Link href={`/projects/${projectId}`} passHref>
- <Button color="primary" outline size="sm" className="mr-2" title="Open this project">
- <FontAwesomeIcon icon={faPlay} />
- </Button>
- </Link>
- <Button
- color="success"
- outline
- size="sm"
- disabled
- className="mr-2"
- title="View and edit collaborators (not supported currently)"
- onClick={() => onViewUsers(projectId)}
- >
- <FontAwesomeIcon icon={faUsers} />
- </Button>
- <Button color="danger" outline size="sm" title="Delete this project" onClick={() => onDelete(projectId)}>
- <FontAwesomeIcon icon={faTrash} />
- </Button>
- </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/ProjectList.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js
deleted file mode 100644
index 46ef4691..00000000
--- a/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { Project } from '../../shapes'
-import ProjectRow from './ProjectRow'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'
-
-const ProjectList = ({ projects }) => {
- return (
- <div className="vertically-expanding-container">
- {projects.length === 0 ? (
- <div className="alert alert-info">
- <FontAwesomeIcon icon={faQuestionCircle} className="info-icon mr-2" />
- <strong>No projects here yet...</strong> Add some with the &lsquo;New Project&rsquo; 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>
- {projects.map((project) => (
- <ProjectRow project={project} key={project._id} />
- ))}
- </tbody>
- </table>
- )}
- </div>
- )
-}
-
-ProjectList.propTypes = {
- projects: PropTypes.arrayOf(Project).isRequired,
-}
-
-export default ProjectList
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js
deleted file mode 100644
index 91368de8..00000000
--- a/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react'
-import ProjectActions from '../../containers/projects/ProjectActions'
-import { Project } from '../../shapes'
-import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from '../../util/authorizations'
-import { parseAndFormatDateTime } from '../../util/date-time'
-import { useAuth } from '../../auth'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-
-const ProjectRow = ({ project }) => {
- const { user } = useAuth()
- const { level } = project.authorizations.find((auth) => auth.userId === user.sub)
- return (
- <tr>
- <td className="pt-3">{project.name}</td>
- <td className="pt-3">{parseAndFormatDateTime(project.datetimeLastEdited)}</td>
- <td className="pt-3">
- <FontAwesomeIcon icon={AUTH_ICON_MAP[level]} className="mr-2" />
- {AUTH_DESCRIPTION_MAP[level]}
- </td>
- <ProjectActions projectId={project._id} />
- </tr>
- )
-}
-
-ProjectRow.propTypes = {
- project: Project.isRequired,
-}
-
-export default ProjectRow
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js
new file mode 100644
index 00000000..a7290259
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectTable.js
@@ -0,0 +1,76 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import Link from 'next/link'
+import { Project, Status } from '../../shapes'
+import { Table, TableBody, TableHeader } from '@patternfly/react-table'
+import { parseAndFormatDateTime } from '../../util/date-time'
+import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from '../../util/authorizations'
+import { useAuth } from '../../auth'
+import TableEmptyState from '../util/TableEmptyState'
+
+const ProjectTable = ({ status, projects, onDelete, isFiltering }) => {
+ const { user } = useAuth()
+ const columns = ['Project name', 'Last edited', 'Access Rights']
+ const rows =
+ projects.length > 0
+ ? projects.map((project) => {
+ const { level } = project.authorizations.find((auth) => auth.userId === user.sub)
+ const Icon = AUTH_ICON_MAP[level]
+ return [
+ {
+ title: <Link href={`/projects/${project._id}`}>{project.name}</Link>,
+ },
+ parseAndFormatDateTime(project.datetimeLastEdited),
+ {
+ title: (
+ <>
+ <Icon className="pf-u-mr-md" key="auth" /> {AUTH_DESCRIPTION_MAP[level]}
+ </>
+ ),
+ },
+ ]
+ })
+ : [
+ {
+ heightAuto: true,
+ cells: [
+ {
+ props: { colSpan: 3 },
+ title: (
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading Projects"
+ isFiltering={isFiltering}
+ />
+ ),
+ },
+ ],
+ },
+ ]
+
+ const actions =
+ projects.length > 0
+ ? [
+ {
+ title: 'Delete Project',
+ onClick: (_, rowId) => onDelete(projects[rowId]),
+ },
+ ]
+ : []
+
+ return (
+ <Table aria-label="Project List" variant="compact" cells={columns} rows={rows} actions={actions}>
+ <TableHeader />
+ <TableBody />
+ </Table>
+ )
+}
+
+ProjectTable.propTypes = {
+ status: Status.isRequired,
+ isFiltering: PropTypes.bool,
+ projects: PropTypes.arrayOf(Project).isRequired,
+ onDelete: PropTypes.func,
+}
+
+export default ProjectTable
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ScenarioState.js b/opendc-web/opendc-web-ui/src/components/projects/ScenarioState.js
new file mode 100644
index 00000000..285345e7
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/ScenarioState.js
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { ClockIcon, InfoIcon, CheckCircleIcon, ErrorCircleOIcon } from '@patternfly/react-icons'
+
+function ScenarioState({ state }) {
+ switch (state) {
+ case 'CLAIMED':
+ case 'QUEUED':
+ return (
+ <span>
+ <ClockIcon color="blue" /> Queued
+ </span>
+ )
+ case 'RUNNING':
+ return (
+ <span>
+ <ClockIcon color="green" /> Running
+ </span>
+ )
+ case 'FINISHED':
+ return (
+ <span>
+ <CheckCircleIcon color="green" /> Finished
+ </span>
+ )
+ case 'FAILED':
+ return (
+ <span>
+ <ErrorCircleOIcon color="red" /> Failed
+ </span>
+ )
+ }
+
+ return 'Unknown'
+}
+
+ScenarioState.propTypes = {
+ state: PropTypes.oneOf(['QUEUED', 'CLAIMED', 'RUNNING', 'FINISHED', 'FAILED']),
+}
+
+export default ScenarioState
diff --git a/opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js b/opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js
new file mode 100644
index 00000000..9966e3ba
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/ScenarioTable.js
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import Link from 'next/link'
+import { Table, TableBody, TableHeader } from '@patternfly/react-table'
+import React from 'react'
+import TableEmptyState from '../util/TableEmptyState'
+import ScenarioState from './ScenarioState'
+import { usePortfolio, usePortfolioScenarios } from '../../data/project'
+import { useProjectTopologies } from '../../data/topology'
+import { useMutation } from 'react-query'
+
+const ScenarioTable = ({ portfolioId }) => {
+ const { data: portfolio } = usePortfolio(portfolioId)
+ const { status, data: scenarios = [] } = usePortfolioScenarios(portfolioId)
+ const { data: topologies } = useProjectTopologies(portfolio?.projectId, {
+ select: (topologies) => new Map(topologies.map((topology) => [topology._id, topology])),
+ })
+
+ const { mutate: deleteScenario } = useMutation('deleteScenario')
+
+ const columns = ['Name', 'Topology', 'Trace', 'State']
+ const rows =
+ scenarios.length > 0
+ ? scenarios.map((scenario) => {
+ const topology = topologies?.get(scenario.topology.topologyId)
+
+ return [
+ scenario.name,
+ {
+ title: topology ? (
+ <Link href={`/projects/${topology.projectId}/topologies/${topology._id}`}>
+ <a>{topology.name}</a>
+ </Link>
+ ) : (
+ 'Unknown Topology'
+ ),
+ },
+ scenario.trace.traceId,
+ { title: <ScenarioState state={scenario.simulation.state} /> },
+ ]
+ })
+ : [
+ {
+ heightAuto: true,
+ cells: [
+ {
+ props: { colSpan: 4 },
+ title: (
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading Scenarios"
+ emptyTitle="No scenarios"
+ emptyText="You have not created any scenario for this portfolio yet. Click the New Scenario button to create one."
+ />
+ ),
+ },
+ ],
+ },
+ ]
+
+ const actionResolver = (_, { rowIndex }) => [
+ {
+ title: 'Delete Scenario',
+ onClick: (_, rowId) => deleteScenario(scenarios[rowId]._id),
+ isDisabled: rowIndex === 0,
+ },
+ ]
+
+ return (
+ <Table
+ aria-label="Scenario List"
+ variant="compact"
+ cells={columns}
+ rows={rows}
+ actionResolver={scenarios.length > 0 ? actionResolver : undefined}
+ >
+ <TableHeader />
+ <TableBody />
+ </Table>
+ )
+}
+
+ScenarioTable.propTypes = {
+ portfolioId: PropTypes.string,
+}
+
+export default ScenarioTable
diff --git a/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
new file mode 100644
index 00000000..80099ece
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import Link from 'next/link'
+import { Table, TableBody, TableHeader } from '@patternfly/react-table'
+import React from 'react'
+import TableEmptyState from '../util/TableEmptyState'
+import { parseAndFormatDateTime } from '../../util/date-time'
+import { useMutation } from 'react-query'
+import { useProjectTopologies } from '../../data/topology'
+
+const TopologyTable = ({ projectId }) => {
+ const { status, data: topologies = [] } = useProjectTopologies(projectId)
+ const { mutate: deleteTopology } = useMutation('deleteTopology')
+
+ const columns = ['Name', 'Rooms', 'Last Edited']
+ const rows =
+ topologies.length > 0
+ ? topologies.map((topology) => [
+ {
+ title: (
+ <Link href={`/projects/${topology.projectId}/topologies/${topology._id}`}>
+ {topology.name}
+ </Link>
+ ),
+ },
+ topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`,
+ parseAndFormatDateTime(topology.datetimeLastEdited),
+ ])
+ : [
+ {
+ heightAuto: true,
+ cells: [
+ {
+ props: { colSpan: 3 },
+ title: (
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading topologies"
+ emptyTitle="No topologies"
+ emptyText="You have not created any topology for this project yet. Click the New Topology button to create one."
+ />
+ ),
+ },
+ ],
+ },
+ ]
+
+ const actionResolver = (_, { rowIndex }) => [
+ {
+ title: 'Delete Topology',
+ onClick: (_, rowId) => deleteTopology(topologies[rowId]._id),
+ isDisabled: rowIndex === 0,
+ },
+ ]
+
+ return (
+ <Table
+ aria-label="Topology List"
+ variant="compact"
+ cells={columns}
+ rows={rows}
+ actionResolver={topologies.length > 0 ? actionResolver : () => []}
+ >
+ <TableHeader />
+ <TableBody />
+ </Table>
+ )
+}
+
+TopologyTable.propTypes = {
+ projectId: PropTypes.string,
+}
+
+export default TopologyTable
diff --git a/opendc-web/opendc-web-ui/src/components/util/BreadcrumbLink.js b/opendc-web/opendc-web-ui/src/components/util/BreadcrumbLink.js
new file mode 100644
index 00000000..c6ab214a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/util/BreadcrumbLink.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import Link from 'next/link'
+
+const BreadcrumbLink = ({ children, href, ...props }) => (
+ <Link href={href}>
+ <a {...props}>{children}</a>
+ </Link>
+)
+
+BreadcrumbLink.propTypes = {
+ children: PropTypes.node,
+ href: PropTypes.string.isRequired,
+}
+
+export default BreadcrumbLink
diff --git a/opendc-web/opendc-web-ui/src/components/util/NavItemLink.js b/opendc-web/opendc-web-ui/src/components/util/NavItemLink.js
new file mode 100644
index 00000000..c0d109bd
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/util/NavItemLink.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import Link from 'next/link'
+import PropTypes from 'prop-types'
+
+const NavItemLink = ({ children, href, ...props }) => (
+ <Link href={href}>
+ <a {...props}>{children}</a>
+ </Link>
+)
+
+NavItemLink.propTypes = {
+ children: PropTypes.node,
+ href: PropTypes.string.isRequired,
+}
+
+export default NavItemLink
diff --git a/opendc-web/opendc-web-ui/src/components/util/TableEmptyState.js b/opendc-web/opendc-web-ui/src/components/util/TableEmptyState.js
new file mode 100644
index 00000000..9d16ffbb
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/util/TableEmptyState.js
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import PropTypes from 'prop-types'
+import { Bullseye, EmptyState, EmptyStateBody, EmptyStateIcon, Spinner, Title } from '@patternfly/react-core'
+import { SearchIcon, CubesIcon } from '@patternfly/react-icons'
+import { Status } from '../../shapes'
+
+function TableEmptyState({
+ status,
+ isFiltering,
+ loadingTitle = 'Loading',
+ emptyTitle = 'No results found',
+ emptyText = 'No results found of this type.',
+ emptyAction = '',
+}) {
+ if (status === 'loading') {
+ return (
+ <Bullseye>
+ <EmptyState variant="xs">
+ <EmptyStateIcon variant="container" component={Spinner} />
+ <Title headingLevel="h4" size="md">
+ {loadingTitle}
+ </Title>
+ </EmptyState>
+ </Bullseye>
+ )
+ } else if (status === 'error') {
+ return (
+ <EmptyState variant="xs">
+ <Title headingLevel="h4" size="md">
+ Unable to connect
+ </Title>
+ <EmptyStateBody>
+ There was an error retrieving data. Check your connection and try again.
+ </EmptyStateBody>
+ </EmptyState>
+ )
+ } else if (status === 'idle') {
+ return (
+ <EmptyState variant="xs">
+ <EmptyStateIcon icon={CubesIcon} />
+ <Title headingLevel="h4" size="md">
+ {emptyTitle}
+ </Title>
+ <EmptyStateBody>No results available at this moment.</EmptyStateBody>
+ </EmptyState>
+ )
+ } else if (isFiltering) {
+ return (
+ <EmptyState variant="xs">
+ <EmptyStateIcon icon={SearchIcon} />
+ <Title headingLevel="h4" size="md">
+ No results found
+ </Title>
+ <EmptyStateBody>
+ No results match this filter criteria. Remove all filters or clear all filters to show results.
+ </EmptyStateBody>
+ </EmptyState>
+ )
+ }
+
+ return (
+ <EmptyState variant="xs">
+ <EmptyStateIcon icon={CubesIcon} />
+ <Title headingLevel="h4" size="md">
+ {emptyTitle}
+ </Title>
+ <EmptyStateBody>{emptyText}</EmptyStateBody>
+ {emptyAction}
+ </EmptyState>
+ )
+}
+
+TableEmptyState.propTypes = {
+ status: Status.isRequired,
+ isFiltering: PropTypes.bool,
+ loadingTitle: PropTypes.string,
+ emptyTitle: PropTypes.string,
+ emptyText: PropTypes.string,
+ emptyAction: PropTypes.node,
+}
+
+export default TableEmptyState
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
deleted file mode 100644
index bac24c8b..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react'
-import { useDispatch } from 'react-redux'
-import { goDownOneInteractionLevel } from '../../../redux/actions/interaction-level'
-import GrayLayer from '../../../components/app/map/elements/GrayLayer'
-
-const GrayContainer = () => {
- const dispatch = useDispatch()
- const onClick = () => dispatch(goDownOneInteractionLevel())
- return <GrayLayer onClick={onClick} />
-}
-
-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
deleted file mode 100644
index 91ceb35d..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react'
-import { useDispatch } from 'react-redux'
-import { setMapDimensions, setMapPositionWithBoundsCheck, zoomInOnPosition } from '../../../redux/actions/map'
-import MapStageComponent from '../../../components/app/map/MapStageComponent'
-import { useMapDimensions, useMapPosition } from '../../../data/map'
-
-const MapStage = () => {
- const position = useMapPosition()
- const dimensions = useMapDimensions()
- const dispatch = useDispatch()
- const zoomInOnPositionA = (zoomIn, x, y) => dispatch(zoomInOnPosition(zoomIn, x, y))
- const setMapPositionWithBoundsCheckA = (x, y) => dispatch(setMapPositionWithBoundsCheck(x, y))
- const setMapDimensionsA = (width, height) => dispatch(setMapDimensions(width, height))
-
- return (
- <MapStageComponent
- mapPosition={position}
- mapDimensions={dimensions}
- zoomInOnPosition={zoomInOnPositionA}
- setMapPositionWithBoundsCheck={setMapPositionWithBoundsCheckA}
- setMapDimensions={setMapDimensionsA}
- />
- )
-}
-
-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
deleted file mode 100644
index e5af5117..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react'
-import { useSelector } from 'react-redux'
-import RackGroup from '../../../components/app/map/groups/RackGroup'
-
-const RackContainer = ({ tile }) => {
- const interactionLevel = useSelector((state) => state.interactionLevel)
- return <RackGroup interactionLeve={interactionLevel} tile={tile} />
-}
-
-export default RackContainer
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
deleted file mode 100644
index 8d6f61e0..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-import { useSelector } from 'react-redux'
-import RackFillBar from '../../../components/app/map/elements/RackFillBar'
-
-const RackSpaceFillContainer = (props) => {
- const state = useSelector((state) => {
- const machineIds = state.objects.rack[state.objects.tile[props.tileId].rack].machines
- return {
- type: 'space',
- fillFraction: machineIds.filter((id) => id !== null).length / machineIds.length,
- }
- })
- return <RackFillBar {...props} {...state} />
-}
-
-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
deleted file mode 100644
index 0a9e1503..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import { goFromBuildingToRoom } from '../../../redux/actions/interaction-level'
-import RoomGroup from '../../../components/app/map/groups/RoomGroup'
-
-const RoomContainer = (props) => {
- const state = useSelector((state) => {
- return {
- interactionLevel: state.interactionLevel,
- currentRoomInConstruction: state.construction.currentRoomInConstruction,
- room: state.objects.room[props.roomId],
- }
- })
- const dispatch = useDispatch()
- return <RoomGroup {...props} {...state} onClick={() => dispatch(goFromBuildingToRoom(props.roomId))} />
-}
-
-RoomContainer.propTypes = {
- roomId: PropTypes.string,
-}
-
-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
deleted file mode 100644
index 50a2abfd..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import { goFromRoomToRack } from '../../../redux/actions/interaction-level'
-import TileGroup from '../../../components/app/map/groups/TileGroup'
-
-const TileContainer = (props) => {
- const interactionLevel = useSelector((state) => state.interactionLevel)
- const tile = useSelector((state) => state.objects.tile[props.tileId])
-
- const dispatch = useDispatch()
- const onClick = (tile) => {
- if (tile.rack) {
- dispatch(goFromRoomToRack(tile._id))
- }
- }
- return <TileGroup {...props} onClick={onClick} tile={tile} interactionLevel={interactionLevel} />
-}
-
-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
deleted file mode 100644
index e7ab3c72..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react'
-import { useSelector } from 'react-redux'
-import TopologyGroup from '../../../components/app/map/groups/TopologyGroup'
-import { useActiveTopology } from '../../../data/topology'
-
-const TopologyContainer = () => {
- const topology = useActiveTopology()
- const interactionLevel = useSelector((state) => state.interactionLevel)
-
- return <TopologyGroup topology={topology} interactionLevel={interactionLevel} />
-}
-
-export default TopologyContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js
deleted file mode 100644
index 67f36396..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react'
-import { useSelector } from 'react-redux'
-import WallGroup from '../../../components/app/map/groups/WallGroup'
-
-const WallContainer = (props) => {
- const tiles = useSelector((state) =>
- state.objects.room[props.roomId].tiles.map((tileId) => state.objects.tile[tileId])
- )
- return <WallGroup {...props} tiles={tiles} />
-}
-
-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
deleted file mode 100644
index a10eea22..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react'
-import ScaleIndicatorComponent from '../../../../components/app/map/controls/ScaleIndicatorComponent'
-import { useMapScale } from '../../../../data/map'
-
-const ScaleIndicatorContainer = (props) => {
- const scale = useMapScale()
- return <ScaleIndicatorComponent {...props} scale={scale} />
-}
-
-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
deleted file mode 100644
index a39c6077..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react'
-import { useDispatch } from 'react-redux'
-import { zoomInOnCenter } from '../../../../redux/actions/map'
-import ZoomControlComponent from '../../../../components/app/map/controls/ZoomControlComponent'
-import { useMapScale } from '../../../../data/map'
-
-const ZoomControlContainer = () => {
- const dispatch = useDispatch()
- const scale = useMapScale()
- return <ZoomControlComponent mapScale={scale} zoomInOnCenter={(zoomIn) => dispatch(zoomInOnCenter(zoomIn))} />
-}
-
-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
deleted file mode 100644
index 633ebcc7..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react'
-import MapLayerComponent from '../../../../components/app/map/layers/MapLayerComponent'
-import { useMapPosition, useMapScale } from '../../../../data/map'
-
-const MapLayer = (props) => {
- const position = useMapPosition()
- const scale = useMapScale()
- return <MapLayerComponent {...props} mapPosition={position} mapScale={scale} />
-}
-
-export default MapLayer
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
deleted file mode 100644
index a75f15ae..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react'
-import PortfolioResultsComponent from '../../../components/app/results/PortfolioResultsComponent'
-import { useRouter } from 'next/router'
-import { usePortfolio, usePortfolioScenarios } from '../../../data/project'
-
-const PortfolioResultsContainer = (props) => {
- const router = useRouter()
- const { portfolio: currentPortfolioId } = router.query
- const { data: portfolio } = usePortfolio(currentPortfolioId)
- const scenarios = usePortfolioScenarios(currentPortfolioId).data ?? []
- return <PortfolioResultsComponent {...props} scenarios={scenarios} portfolio={portfolio} />
-}
-
-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
deleted file mode 100644
index 60ac666c..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import React, { useState } from 'react'
-import { useRouter } from 'next/router'
-import PortfolioListComponent from '../../../../components/app/sidebars/project/PortfolioListComponent'
-import NewPortfolioModalComponent from '../../../../components/modals/custom-components/NewPortfolioModalComponent'
-import { useProjectPortfolios } from '../../../../data/project'
-import { useMutation } from 'react-query'
-
-const PortfolioListContainer = () => {
- const router = useRouter()
- const { project: currentProjectId, portfolio: currentPortfolioId } = router.query
- const portfolios = useProjectPortfolios(currentProjectId).data ?? []
-
- const { mutate: addPortfolio } = useMutation('addPortfolio')
- const { mutateAsync: deletePortfolio } = useMutation('deletePortfolio')
-
- const [isVisible, setVisible] = useState(false)
- const actions = {
- onNewPortfolio: () => setVisible(true),
- onChoosePortfolio: async (portfolioId) => {
- await router.push(`/projects/${currentProjectId}/portfolios/${portfolioId}`)
- },
- onDeletePortfolio: async (id) => {
- if (id) {
- await deletePortfolio(id)
- await router.push(`/projects/${currentProjectId}`)
- }
- },
- }
- const callback = (name, targets) => {
- if (name) {
- addPortfolio({ projectId: currentProjectId, name, targets })
- }
- setVisible(false)
- }
- return (
- <>
- <PortfolioListComponent
- currentProjectId={currentProjectId}
- currentPortfolioId={currentPortfolioId}
- portfolios={portfolios}
- {...actions}
- />
- <NewPortfolioModalComponent callback={callback} show={isVisible} />
- </>
- )
-}
-
-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
deleted file mode 100644
index 06c7f0f7..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react'
-import { useRouter } from 'next/router'
-import ProjectSidebarComponent from '../../../../components/app/sidebars/project/ProjectSidebarComponent'
-import { isCollapsible } from '../../../../util/sidebar-space'
-
-const ProjectSidebarContainer = (props) => {
- const router = useRouter()
- return <ProjectSidebarComponent collapsible={isCollapsible(router)} {...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
deleted file mode 100644
index 3b68df38..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import PropTypes from 'prop-types'
-import React, { useState } from 'react'
-import ScenarioListComponent from '../../../../components/app/sidebars/project/ScenarioListComponent'
-import NewScenarioModalComponent from '../../../../components/modals/custom-components/NewScenarioModalComponent'
-import { useProjectTopologies } from '../../../../data/topology'
-import { usePortfolio, usePortfolioScenarios } from '../../../../data/project'
-import { useSchedulers, useTraces } from '../../../../data/experiments'
-import { useMutation } from 'react-query'
-
-const ScenarioListContainer = ({ portfolioId }) => {
- const { data: portfolio } = usePortfolio(portfolioId)
- const scenarios = usePortfolioScenarios(portfolioId).data ?? []
- const topologies =
- useProjectTopologies(portfolio?.projectId).data?.map((topology) => ({
- _id: topology._id,
- name: topology.name,
- })) ?? []
- const traces = useTraces().data ?? []
- const schedulers = useSchedulers().data ?? []
-
- const { mutate: addScenario } = useMutation('addScenario')
- const { mutate: deleteScenario } = useMutation('deleteScenario')
-
- const [isVisible, setVisible] = useState(false)
-
- const onNewScenario = () => setVisible(true)
- const onDeleteScenario = (id) => id && deleteScenario(id)
- const callback = (name, portfolioId, trace, topology, operational) => {
- if (name) {
- addScenario({
- portfolioId,
- name,
- trace,
- topology,
- operational,
- })
- }
-
- setVisible(false)
- }
-
- return (
- <>
- <ScenarioListComponent
- portfolioId={portfolioId}
- scenarios={scenarios}
- onNewScenario={onNewScenario}
- onDeleteScenario={onDeleteScenario}
- />
- <NewScenarioModalComponent
- show={isVisible}
- currentPortfolioId={portfolioId}
- currentPortfolioScenarioIds={scenarios.map((s) => s._id)}
- traces={traces}
- schedulers={schedulers}
- topologies={topologies}
- callback={callback}
- />
- </>
- )
-}
-
-ScenarioListContainer.propTypes = {
- portfolioId: PropTypes.string,
-}
-
-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
deleted file mode 100644
index a2244a30..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import React, { useState } from 'react'
-import { useDispatch } from 'react-redux'
-import TopologyListComponent from '../../../../components/app/sidebars/project/TopologyListComponent'
-import { setCurrentTopology } from '../../../../redux/actions/topology/building'
-import { useRouter } from 'next/router'
-import { addTopology } from '../../../../redux/actions/topologies'
-import NewTopologyModalComponent from '../../../../components/modals/custom-components/NewTopologyModalComponent'
-import { useActiveTopology, useProjectTopologies } from '../../../../data/topology'
-import { useMutation } from 'react-query'
-
-const TopologyListContainer = () => {
- const dispatch = useDispatch()
- const router = useRouter()
- const { project: currentProjectId } = router.query
- const topologies =
- useProjectTopologies(currentProjectId).data?.map((topology) => ({ _id: topology._id, name: topology.name })) ??
- []
- const currentTopologyId = useActiveTopology()?._id
- const [isVisible, setVisible] = useState(false)
-
- const { mutate: deleteTopology } = useMutation('deleteTopology')
-
- const onChooseTopology = async (id) => {
- dispatch(setCurrentTopology(id))
- await router.push(`/projects/${currentProjectId}/topologies/${id}`)
- }
- const onDeleteTopology = async (id) => {
- if (id) {
- deleteTopology(id)
- await router.push(`/projects/${currentProjectId}`)
- }
- }
- const onCreateTopology = (name) => {
- if (name) {
- dispatch(addTopology(currentProjectId, name, undefined))
- }
- setVisible(false)
- }
- const onDuplicateTopology = (name, id) => {
- if (name) {
- dispatch(addTopology(currentProjectId, name, id))
- }
- setVisible(false)
- }
- const onCancel = () => setVisible(false)
-
- return (
- <>
- <TopologyListComponent
- topologies={topologies}
- currentTopologyId={currentTopologyId}
- onChooseTopology={onChooseTopology}
- onNewTopology={() => setVisible(true)}
- onDeleteTopology={onDeleteTopology}
- />
- <NewTopologyModalComponent
- show={isVisible}
- topologies={topologies}
- onCreateTopology={onCreateTopology}
- onDuplicateTopology={onDuplicateTopology}
- onCancel={onCancel}
- />
- </>
- )
-}
-
-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
deleted file mode 100644
index 42c81c65..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react'
-import { useSelector } from 'react-redux'
-import TopologySidebarComponent from '../../../../components/app/sidebars/topology/TopologySidebarComponent'
-
-const TopologySidebarContainer = (props) => {
- const interactionLevel = useSelector((state) => state.interactionLevel)
- return <TopologySidebarComponent {...props} interactionLevel={interactionLevel} />
-}
-
-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
deleted file mode 100644
index a0b52e56..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js
+++ /dev/null
@@ -1,5 +0,0 @@
-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
deleted file mode 100644
index 96f42a44..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import {
- cancelNewRoomConstruction,
- finishNewRoomConstruction,
- startNewRoomConstruction,
-} from '../../../../../redux/actions/topology/building'
-import StartNewRoomConstructionComponent from '../../../../../components/app/sidebars/topology/building/NewRoomConstructionComponent'
-
-const NewRoomConstructionButton = (props) => {
- const currentRoomInConstruction = useSelector((state) => state.construction.currentRoomInConstruction)
-
- const dispatch = useDispatch()
- const actions = {
- onStart: () => dispatch(startNewRoomConstruction()),
- onFinish: () => dispatch(finishNewRoomConstruction()),
- onCancel: () => dispatch(cancelNewRoomConstruction()),
- }
- return (
- <StartNewRoomConstructionComponent
- {...props}
- {...actions}
- currentRoomInConstruction={currentRoomInConstruction}
- />
- )
-}
-
-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
deleted file mode 100644
index ea250767..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react'
-import { useDispatch } from 'react-redux'
-import { goDownOneInteractionLevel } from '../../../../../redux/actions/interaction-level'
-import BackToRackComponent from '../../../../../components/app/sidebars/topology/machine/BackToRackComponent'
-
-const BackToRackContainer = (props) => {
- const dispatch = useDispatch()
- return <BackToRackComponent {...props} onClick={() => dispatch(goDownOneInteractionLevel())} />
-}
-
-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
deleted file mode 100644
index 54e406f4..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React, { useState } from 'react'
-import { useDispatch } from 'react-redux'
-import ConfirmationModal from '../../../../../components/modals/ConfirmationModal'
-import { deleteMachine } from '../../../../../redux/actions/topology/machine'
-import { Button } from 'reactstrap'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faTrash } from '@fortawesome/free-solid-svg-icons'
-
-const DeleteMachineContainer = () => {
- const dispatch = useDispatch()
- const [isVisible, setVisible] = useState(false)
- const callback = (isConfirmed) => {
- if (isConfirmed) {
- dispatch(deleteMachine())
- }
- setVisible(false)
- }
- return (
- <>
- <Button color="danger" outline block onClick={() => setVisible(true)}>
- <FontAwesomeIcon icon={faTrash} className="mr-2" />
- Delete this machine
- </Button>
- <ConfirmationModal
- title="Delete this machine"
- message="Are you sure you want to delete this machine?"
- show={isVisible}
- callback={callback}
- />
- </>
- )
-}
-
-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
deleted file mode 100644
index 9cbb32c5..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react'
-import { useSelector } from 'react-redux'
-
-const MachineNameContainer = () => {
- const position = useSelector((state) => state.interactionLevel.position)
- return <h2>Machine at slot {position}</h2>
-}
-
-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
deleted file mode 100644
index 7553c2fe..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import { useSelector } from 'react-redux'
-import MachineSidebarComponent from '../../../../../components/app/sidebars/topology/machine/MachineSidebarComponent'
-
-const MachineSidebarContainer = (props) => {
- const machineId = useSelector(
- (state) =>
- state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].machines[
- state.interactionLevel.position - 1
- ]
- )
- return <MachineSidebarComponent {...props} machineId={machineId} />
-}
-
-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
deleted file mode 100644
index 0f85aa76..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import { addUnit } from '../../../../../redux/actions/topology/machine'
-import UnitAddComponent from '../../../../../components/app/sidebars/topology/machine/UnitAddComponent'
-
-const UnitAddContainer = (props) => {
- const units = useSelector((state) => Object.values(state.objects[props.unitType]))
- const dispatch = useDispatch()
-
- const onAdd = (id) => dispatch(addUnit(props.unitType, id))
-
- return <UnitAddComponent {...props} onAdd={onAdd} units={units} />
-}
-
-export default UnitAddContainer
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
deleted file mode 100644
index cdd7e268..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import PropTypes from 'prop-types'
-import React from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import UnitListComponent from '../../../../../components/app/sidebars/topology/machine/UnitListComponent'
-import { deleteUnit } from '../../../../../redux/actions/topology/machine'
-
-const unitMapping = {
- cpu: 'cpus',
- gpu: 'gpus',
- memory: 'memories',
- storage: 'storages',
-}
-
-const UnitListContainer = ({ unitType, ...props }) => {
- const dispatch = useDispatch()
- const units = useSelector((state) => {
- const machine =
- state.objects.machine[
- state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].machines[
- state.interactionLevel.position - 1
- ]
- ]
- return machine[unitMapping[unitType]].map((id) => state.objects[unitType][id])
- })
- const onDelete = (unit, unitType) => dispatch(deleteUnit(unitType, unit._id))
-
- return <UnitListComponent {...props} units={units} unitType={unitType} onDelete={onDelete} />
-}
-
-UnitListContainer.propTypes = {
- unitType: PropTypes.string.isRequired,
-}
-
-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
deleted file mode 100644
index 00fe4067..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js
+++ /dev/null
@@ -1,5 +0,0 @@
-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
deleted file mode 100644
index c2a0fc48..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react'
-import { useDispatch } from 'react-redux'
-import { addPrefab } from '../../../../../redux/actions/prefabs'
-import AddPrefabComponent from '../../../../../components/app/sidebars/topology/rack/AddPrefabComponent'
-
-const AddPrefabContainer = (props) => {
- const dispatch = useDispatch()
- return <AddPrefabComponent {...props} onClick={() => dispatch(addPrefab('name'))} />
-}
-
-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
deleted file mode 100644
index a98728a6..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react'
-import { useDispatch } from 'react-redux'
-import { goDownOneInteractionLevel } from '../../../../../redux/actions/interaction-level'
-import BackToRoomComponent from '../../../../../components/app/sidebars/topology/rack/BackToRoomComponent'
-
-const BackToRoomContainer = (props) => {
- const dispatch = useDispatch()
- return <BackToRoomComponent {...props} onClick={() => dispatch(goDownOneInteractionLevel())} />
-}
-
-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
deleted file mode 100644
index 4463530e..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React, { useState } from 'react'
-import { useDispatch } from 'react-redux'
-import ConfirmationModal from '../../../../../components/modals/ConfirmationModal'
-import { deleteRack } from '../../../../../redux/actions/topology/rack'
-import { Button } from 'reactstrap'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faTrash } from '@fortawesome/free-solid-svg-icons'
-
-const DeleteRackContainer = () => {
- const dispatch = useDispatch()
- const [isVisible, setVisible] = useState(false)
- const callback = (isConfirmed) => {
- if (isConfirmed) {
- dispatch(deleteRack())
- }
- setVisible(false)
- }
- return (
- <>
- <Button color="danger" outline block onClick={() => setVisible(true)}>
- <FontAwesomeIcon icon={faTrash} className="mr-2" />
- Delete this rack
- </Button>
- <ConfirmationModal
- title="Delete this rack"
- message="Are you sure you want to delete this rack?"
- show={isVisible}
- callback={callback}
- />
- </>
- )
-}
-
-export default DeleteRackContainer
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
deleted file mode 100644
index 2118d915..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React, { useMemo } from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import MachineListComponent from '../../../../../components/app/sidebars/topology/rack/MachineListComponent'
-import { goFromRackToMachine } from '../../../../../redux/actions/interaction-level'
-import { addMachine } from '../../../../../redux/actions/topology/rack'
-
-const MachineListContainer = (props) => {
- const rack = useSelector((state) => state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack])
- const machines = useSelector((state) => rack.machines.map((id) => state.objects.machine[id]))
- const machinesNull = useMemo(() => {
- const res = Array(rack.capacity).fill(null)
- for (const machine of machines) {
- res[machine.position - 1] = machine
- }
- return res
- }, [rack, machines])
- const dispatch = useDispatch()
-
- return (
- <MachineListComponent
- {...props}
- machines={machinesNull}
- onAdd={(index) => dispatch(addMachine(index))}
- onSelect={(index) => dispatch(goFromRackToMachine(index))}
- />
- )
-}
-
-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
deleted file mode 100644
index 2c39cf9f..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import React, { useState } from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import NameComponent from '../../../../../components/app/sidebars/topology/NameComponent'
-import TextInputModal from '../../../../../components/modals/TextInputModal'
-import { editRackName } from '../../../../../redux/actions/topology/rack'
-
-const RackNameContainer = () => {
- const [isVisible, setVisible] = useState(false)
- const rackName = useSelector(
- (state) => state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].name
- )
- const dispatch = useDispatch()
- const callback = (name) => {
- if (name) {
- dispatch(editRackName(name))
- }
- setVisible(false)
- }
- return (
- <>
- <NameComponent name={rackName} onEdit={() => setVisible(true)} />
- <TextInputModal
- title="Edit rack name"
- label="Rack name"
- show={isVisible}
- initialValue={rackName}
- callback={callback}
- />
- </>
- )
-}
-
-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
deleted file mode 100644
index 34777125..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react'
-import { useSelector } from 'react-redux'
-import RackSidebarComponent from '../../../../../components/app/sidebars/topology/rack/RackSidebarComponent'
-
-const RackSidebarContainer = (props) => {
- const rackId = useSelector((state) => state.objects.tile[state.interactionLevel.tileId].rack)
- return <RackSidebarComponent {...props} rackId={rackId} />
-}
-
-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
deleted file mode 100644
index 9fa1e95f..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react'
-import { useDispatch } from 'react-redux'
-import { goDownOneInteractionLevel } from '../../../../../redux/actions/interaction-level'
-import BackToBuildingComponent from '../../../../../components/app/sidebars/topology/room/BackToBuildingComponent'
-
-const BackToBuildingContainer = () => {
- const dispatch = useDispatch()
- const onClick = () => dispatch(goDownOneInteractionLevel())
- return <BackToBuildingComponent onClick={onClick} />
-}
-
-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
deleted file mode 100644
index 0fbbb036..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React, { useState } from 'react'
-import { useDispatch } from 'react-redux'
-import { Button } from 'reactstrap'
-import ConfirmationModal from '../../../../../components/modals/ConfirmationModal'
-import { deleteRoom } from '../../../../../redux/actions/topology/room'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faTrash } from '@fortawesome/free-solid-svg-icons'
-
-const DeleteRoomContainer = () => {
- const dispatch = useDispatch()
- const [isVisible, setVisible] = useState(false)
- const callback = (isConfirmed) => {
- if (isConfirmed) {
- dispatch(deleteRoom())
- }
- setVisible(false)
- }
- return (
- <>
- <Button color="danger" outline block onClick={() => setVisible(true)}>
- <FontAwesomeIcon icon={faTrash} className="mr-2" />
- Delete this room
- </Button>
- <ConfirmationModal
- title="Delete this room"
- message="Are you sure you want to delete this room?"
- show={isVisible}
- callback={callback}
- />
- </>
- )
-}
-
-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
deleted file mode 100644
index ec4f586b..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import { finishRoomEdit, startRoomEdit } from '../../../../../redux/actions/topology/building'
-import { Button } from 'reactstrap'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faCheck, faPencilAlt } from '@fortawesome/free-solid-svg-icons'
-
-const EditRoomContainer = () => {
- const isEditing = useSelector((state) => state.construction.currentRoomInConstruction !== '-1')
- const isInRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode)
-
- const dispatch = useDispatch()
- const onEdit = () => dispatch(startRoomEdit())
- const onFinish = () => dispatch(finishRoomEdit())
-
- return isEditing ? (
- <Button color="info" outline block onClick={onFinish}>
- <FontAwesomeIcon icon={faCheck} className="mr-2" />
- Finish editing room
- </Button>
- ) : (
- <Button
- color="info"
- outline
- block
- disabled={isInRackConstructionMode}
- onClick={() => (isInRackConstructionMode ? undefined : onEdit())}
- >
- <FontAwesomeIcon icon={faPencilAlt} className="mr-2" />
- Edit the tiles of this room
- </Button>
- )
-}
-
-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
deleted file mode 100644
index 79584e98..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import { startRackConstruction, stopRackConstruction } from '../../../../../redux/actions/topology/room'
-import RackConstructionComponent from '../../../../../components/app/sidebars/topology/room/RackConstructionComponent'
-
-const RackConstructionContainer = (props) => {
- const isRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode)
- const isEditingRoom = useSelector((state) => state.construction.currentRoomInConstruction !== '-1')
-
- const dispatch = useDispatch()
- const onStart = () => dispatch(startRackConstruction())
- const onStop = () => dispatch(stopRackConstruction())
- return (
- <RackConstructionComponent
- {...props}
- inRackConstructionMode={isRackConstructionMode}
- isEditingRoom={isEditingRoom}
- onStart={onStart}
- onStop={onStop}
- />
- )
-}
-
-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
deleted file mode 100644
index 3b35a849..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import React, { useState } from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import NameComponent from '../../../../../components/app/sidebars/topology/NameComponent'
-import TextInputModal from '../../../../../components/modals/TextInputModal'
-import { editRoomName } from '../../../../../redux/actions/topology/room'
-
-const RoomNameContainer = () => {
- const [isVisible, setVisible] = useState(false)
- const roomName = useSelector((state) => state.objects.room[state.interactionLevel.roomId].name)
- const dispatch = useDispatch()
- const callback = (name) => {
- if (name) {
- dispatch(editRoomName(name))
- }
- setVisible(false)
- }
- return (
- <>
- <NameComponent name={roomName} onEdit={() => setVisible(true)} />
- <TextInputModal
- title="Edit room name"
- label="Room name"
- show={isVisible}
- initialValue={roomName}
- callback={callback}
- />
- </>
- )
-}
-
-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
deleted file mode 100644
index 252881a0..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react'
-import { useSelector } from 'react-redux'
-import RoomSidebarComponent from '../../../../../components/app/sidebars/topology/room/RoomSidebarComponent'
-
-const RoomSidebarContainer = (props) => {
- const roomId = useSelector((state) => state.interactionLevel.roomId)
- return <RoomSidebarComponent {...props} roomId={roomId} />
-}
-
-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
deleted file mode 100644
index d8083d89..00000000
--- a/opendc-web/opendc-web-ui/src/containers/auth/Login.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react'
-import { Button } from 'reactstrap'
-import { useAuth } from '../../auth'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
-
-function Login({ visible, className }) {
- const { loginWithRedirect } = useAuth()
-
- if (!visible) {
- return <span />
- }
-
- return (
- <Button color="primary" onClick={() => loginWithRedirect()} className={className}>
- <FontAwesomeIcon icon={faSignInAlt} /> Sign In
- </Button>
- )
-}
-
-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
deleted file mode 100644
index 37705c5d..00000000
--- a/opendc-web/opendc-web-ui/src/containers/auth/Logout.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react'
-import LogoutButton from '../../components/navigation/LogoutButton'
-import { useAuth } from '../../auth'
-
-const Logout = (props) => {
- const { logout } = useAuth()
- return <LogoutButton {...props} onLogout={() => logout({ returnTo: window.location.origin })} />
-}
-
-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
deleted file mode 100644
index 70f5b884..00000000
--- a/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react'
-import { useAuth } from '../../auth'
-
-function ProfileName() {
- const { isLoading, user } = useAuth()
- return isLoading ? <span>Loading...</span> : <span>{user.name}</span>
-}
-
-export default ProfileName
diff --git a/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js b/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js
deleted file mode 100644
index ff9f9fe7..00000000
--- a/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react'
-import AppNavbarComponent from '../../components/navigation/AppNavbarComponent'
-import { useActiveProjectId, useProject } from '../../data/project'
-
-const AppNavbarContainer = (props) => {
- const projectId = useActiveProjectId()
- const { data: project } = useProject(projectId)
- return <AppNavbarComponent {...props} project={project} />
-}
-
-export default AppNavbarContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js b/opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js
deleted file mode 100644
index ac0edae4..00000000
--- a/opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React, { useState } from 'react'
-import TextInputModal from '../../components/modals/TextInputModal'
-import { Button } from 'reactstrap'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faPlus } from '@fortawesome/free-solid-svg-icons'
-import { useMutation } from 'react-query'
-
-/**
- * A container for creating a new project.
- */
-const NewProjectContainer = () => {
- const [isVisible, setVisible] = useState(false)
- const { mutate: addProject } = useMutation('addProject')
- const callback = (text) => {
- if (text) {
- addProject({ name: text })
- }
- setVisible(false)
- }
-
- return (
- <>
- <div className="bottom-btn-container">
- <Button color="primary" className="float-right" onClick={() => setVisible(true)}>
- <FontAwesomeIcon icon={faPlus} className="mr-2" />
- New Project
- </Button>
- </div>
- <TextInputModal title="New Project" label="Project title" show={isVisible} callback={callback} />
- </>
- )
-}
-
-export default NewProjectContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js b/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js
deleted file mode 100644
index 62985742..00000000
--- a/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react'
-import ProjectActionButtons from '../../components/projects/ProjectActionButtons'
-import { useMutation } from 'react-query'
-
-const ProjectActions = (props) => {
- const { mutate: deleteProject } = useMutation('deleteProject')
- const actions = {
- onViewUsers: (id) => {}, // TODO implement user viewing
- onDelete: (id) => deleteProject(id),
- }
- return <ProjectActionButtons {...props} {...actions} />
-}
-
-export default ProjectActions
diff --git a/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js b/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js
deleted file mode 100644
index b5c5dd68..00000000
--- a/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import ProjectList from '../../components/projects/ProjectList'
-import { useAuth } from '../../auth'
-import { useProjects } from '../../data/project'
-
-const getVisibleProjects = (projects, filter, userId) => {
- switch (filter) {
- case 'SHOW_ALL':
- return projects
- case 'SHOW_OWN':
- return projects.filter((project) =>
- project.authorizations.some((a) => a.userId === userId && a.level === 'OWN')
- )
- case 'SHOW_SHARED':
- return projects.filter((project) =>
- project.authorizations.some((a) => a.userId === userId && a.level !== 'OWN')
- )
- default:
- return projects
- }
-}
-
-const ProjectListContainer = ({ filter }) => {
- const { user } = useAuth()
- const { data: projects } = useProjects()
- return <ProjectList projects={getVisibleProjects(projects ?? [], filter, user?.sub)} />
-}
-
-ProjectListContainer.propTypes = {
- filter: PropTypes.string.isRequired,
-}
-
-export default ProjectListContainer
diff --git a/opendc-web/opendc-web-ui/src/data/map.js b/opendc-web/opendc-web-ui/src/data/map.js
index 6aef6ac5..348a6664 100644
--- a/opendc-web/opendc-web-ui/src/data/map.js
+++ b/opendc-web/opendc-web-ui/src/data/map.js
@@ -35,7 +35,3 @@ export function useMapScale() {
export function useMapPosition() {
return useSelector((state) => state.map.position)
}
-
-export function useMapDimensions() {
- return useSelector((state) => state.map.dimensions)
-}
diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js
index 9bdcfb93..9dcd8532 100644
--- a/opendc-web/opendc-web-ui/src/data/project.js
+++ b/opendc-web/opendc-web-ui/src/data/project.js
@@ -20,9 +20,8 @@
* SOFTWARE.
*/
-import { useQueries, useQuery } from 'react-query'
+import { useQuery } from 'react-query'
import { addProject, deleteProject, fetchProject, fetchProjects } from '../api/projects'
-import { useRouter } from 'next/router'
import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfoliosOfProject } from '../api/portfolios'
import { addScenario, deleteScenario, fetchScenario, fetchScenariosOfPortfolio } from '../api/scenarios'
@@ -38,6 +37,7 @@ export function configureProjectClient(queryClient, auth) {
mutationFn: (data) => addProject(auth, data),
onSuccess: async (result) => {
queryClient.setQueryData('projects', (old = []) => [...old, result])
+ queryClient.setQueryData(['projects', result._id], result)
},
})
queryClient.setMutationDefaults('deleteProject', {
@@ -61,6 +61,7 @@ export function configureProjectClient(queryClient, auth) {
...old,
portfolioIds: [...old.portfolioIds, result._id],
}))
+ queryClient.setQueryData(['project-portfolios', result.projectId], (old = []) => [...old, result])
queryClient.setQueryData(['portfolios', result._id], result)
},
})
@@ -71,6 +72,9 @@ export function configureProjectClient(queryClient, auth) {
...old,
portfolioIds: old.portfolioIds.filter((id) => id !== result._id),
}))
+ queryClient.setQueryData(['project-portfolios', result.projectId], (old = []) =>
+ old.filter((portfolio) => portfolio._id !== result._id)
+ )
queryClient.removeQueries(['portfolios', result._id])
},
})
@@ -86,6 +90,7 @@ export function configureProjectClient(queryClient, auth) {
onSuccess: async (result) => {
// Register updated scenario in cache
queryClient.setQueryData(['scenarios', result._id], result)
+ queryClient.setQueryData(['portfolio-scenarios', result.portfolioId], (old = []) => [...old, result])
// Add scenario id to portfolio
queryClient.setQueryData(['portfolios', result.portfolioId], (old) => ({
@@ -101,6 +106,9 @@ export function configureProjectClient(queryClient, auth) {
...old,
scenarioIds: old.scenarioIds.filter((id) => id !== result._id),
}))
+ queryClient.setQueryData(['portfolio-scenarios', result.portfolioId], (old = []) =>
+ old.filter((scenario) => scenario._id !== result._id)
+ )
queryClient.removeQueries(['scenarios', result._id])
},
})
@@ -109,54 +117,34 @@ export function configureProjectClient(queryClient, auth) {
/**
* Return the available projects.
*/
-export function useProjects() {
- return useQuery('projects')
+export function useProjects(options = {}) {
+ return useQuery('projects', options)
}
/**
* Return the project with the specified identifier.
*/
-export function useProject(projectId) {
- return useQuery(['projects', projectId], { enabled: !!projectId })
+export function useProject(projectId, options = {}) {
+ return useQuery(['projects', projectId], { enabled: !!projectId, ...options })
}
/**
* Return the portfolio with the specified identifier.
*/
-export function usePortfolio(portfolioId) {
- return useQuery(['portfolios', portfolioId], { enabled: !!portfolioId })
+export function usePortfolio(portfolioId, options = {}) {
+ return useQuery(['portfolios', portfolioId], { enabled: !!portfolioId, ...options })
}
/**
* Return the portfolios of the specified project.
*/
-export function useProjectPortfolios(projectId) {
- return useQuery(['project-portfolios', projectId], { enabled: !!projectId })
-}
-
-/**
- * Return the scenarios with the specified identifiers.
- */
-export function useScenarios(scenarioIds) {
- return useQueries(
- scenarioIds.map((scenarioId) => ({
- queryKey: ['scenarios', scenarioId],
- }))
- )
+export function useProjectPortfolios(projectId, options = {}) {
+ return useQuery(['project-portfolios', projectId], { enabled: !!projectId, ...options })
}
/**
* Return the scenarios of the specified portfolio.
*/
-export function usePortfolioScenarios(portfolioId) {
- return useQuery(['portfolio-scenarios', portfolioId], { enabled: !!portfolioId })
-}
-
-/**
- * Return the current active project identifier.
- */
-export function useActiveProjectId() {
- const router = useRouter()
- const { project } = router.query
- return project
+export function usePortfolioScenarios(portfolioId, options = {}) {
+ return useQuery(['portfolio-scenarios', portfolioId], { enabled: !!portfolioId, ...options })
}
diff --git a/opendc-web/opendc-web-ui/src/data/topology.js b/opendc-web/opendc-web-ui/src/data/topology.js
index 8db75877..14bd7562 100644
--- a/opendc-web/opendc-web-ui/src/data/topology.js
+++ b/opendc-web/opendc-web-ui/src/data/topology.js
@@ -21,7 +21,7 @@
*/
import { useSelector } from 'react-redux'
-import { useQueries, useQuery } from 'react-query'
+import { useQuery } from 'react-query'
import { addTopology, deleteTopology, fetchTopologiesOfProject, fetchTopology, updateTopology } from '../api/topologies'
/**
@@ -40,6 +40,7 @@ export function configureTopologyClient(queryClient, auth) {
...old,
topologyIds: [...old.topologyIds, result._id],
}))
+ queryClient.setQueryData(['project-topologies', result.projectId], (old = []) => [...old, result])
queryClient.setQueryData(['topologies', result._id], result)
},
})
@@ -54,6 +55,9 @@ export function configureTopologyClient(queryClient, auth) {
...old,
topologyIds: old.topologyIds.filter((id) => id !== result._id),
}))
+ queryClient.setQueryData(['project-topologies', result.projectId], (old = []) =>
+ old.filter((topology) => topology._id !== result._id)
+ )
queryClient.removeQueries(['topologies', result._id])
},
})
@@ -69,6 +73,6 @@ export function useActiveTopology() {
/**
* Return the topologies of the specified project.
*/
-export function useProjectTopologies(projectId) {
- return useQuery(['project-topologies', projectId], { enabled: !!projectId })
+export function useProjectTopologies(projectId, options = {}) {
+ return useQuery(['project-topologies', projectId], { enabled: !!projectId, ...options })
}
diff --git a/opendc-web/opendc-web-ui/src/index.scss b/opendc-web/opendc-web-ui/src/index.scss
deleted file mode 100644
index dbd9550c..00000000
--- a/opendc-web/opendc-web-ui/src/index.scss
+++ /dev/null
@@ -1,68 +0,0 @@
-@import '~bootstrap/scss/bootstrap';
-
-@import './style/_mixins.scss';
-@import './style/_variables.scss';
-
-html,
-body,
-#__next {
- 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 {
- @include 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/404.js b/opendc-web/opendc-web-ui/src/pages/404.js
index cc9281fc..0939bc56 100644
--- a/opendc-web/opendc-web-ui/src/pages/404.js
+++ b/opendc-web/opendc-web-ui/src/pages/404.js
@@ -1,18 +1,37 @@
import React from 'react'
import Head from 'next/head'
-import TerminalWindow from '../components/not-found/TerminalWindow'
-import style from './404.module.scss'
+import { AppPage } from '../components/AppPage'
+import {
+ Bullseye,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ PageSection,
+ PageSectionVariants,
+ Title,
+} from '@patternfly/react-core'
+import { UnknownIcon } from '@patternfly/react-icons'
const NotFound = () => {
return (
- <>
+ <AppPage>
<Head>
<title>Page Not Found - OpenDC</title>
</Head>
- <div className={style['not-found-backdrop']}>
- <TerminalWindow />
- </div>
- </>
+ <PageSection variant={PageSectionVariants.light}>
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={UnknownIcon} />
+ <Title size="lg" headingLevel="h4">
+ 404: That page does not exist
+ </Title>
+ <EmptyStateBody>
+ The requested page is not found. Try refreshing the page if it was recently added.
+ </EmptyStateBody>
+ </EmptyState>
+ </Bullseye>
+ </PageSection>
+ </AppPage>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/404.module.scss b/opendc-web/opendc-web-ui/src/pages/404.module.scss
deleted file mode 100644
index e91c2780..00000000
--- a/opendc-web/opendc-web-ui/src/pages/404.module.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.not-found-backdrop {
- display: flex;
-
- width: 100%;
- height: 100%;
-
- background-image: linear-gradient(135deg, #00678a, #008fbf, #00a6d6);
-}
diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js
index 6a7200d5..028dab10 100644
--- a/opendc-web/opendc-web-ui/src/pages/_app.js
+++ b/opendc-web/opendc-web-ui/src/pages/_app.js
@@ -24,7 +24,6 @@ import PropTypes from 'prop-types'
import Head from 'next/head'
import { Provider } from 'react-redux'
import { useStore } from '../redux'
-import '../index.scss'
import { AuthProvider, useAuth } from '../auth'
import * as Sentry from '@sentry/react'
import { Integrations } from '@sentry/tracing'
@@ -34,6 +33,19 @@ import { configureProjectClient } from '../data/project'
import { configureExperimentClient } from '../data/experiments'
import { configureTopologyClient } from '../data/topology'
+import '@patternfly/react-core/dist/styles/base.css'
+import '@patternfly/react-styles/css/utilities/Alignment/alignment.css'
+import '@patternfly/react-styles/css/utilities/BackgroundColor/BackgroundColor.css'
+import '@patternfly/react-styles/css/utilities/BoxShadow/box-shadow.css'
+import '@patternfly/react-styles/css/utilities/Display/display.css'
+import '@patternfly/react-styles/css/utilities/Flex/flex.css'
+import '@patternfly/react-styles/css/utilities/Float/float.css'
+import '@patternfly/react-styles/css/utilities/Sizing/sizing.css'
+import '@patternfly/react-styles/css/utilities/Spacing/spacing.css'
+import '@patternfly/react-styles/css/utilities/Text/text.css'
+import '@patternfly/react-styles/css/components/InlineEdit/inline-edit.css'
+import '../style/index.scss'
+
// This setup is necessary to forward the Auth0 context to the Redux context
const Inner = ({ Component, pageProps }) => {
const auth = useAuth()
diff --git a/opendc-web/opendc-web-ui/src/pages/index.js b/opendc-web/opendc-web-ui/src/pages/index.js
deleted file mode 100644
index bb904eb6..00000000
--- a/opendc-web/opendc-web-ui/src/pages/index.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react'
-import Head from 'next/head'
-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 {
- introSection,
- stakeholderSection,
- modelingSection,
- simulationSection,
- technologiesSection,
- teamSection,
-} from './index.module.scss'
-
-function Home() {
- return (
- <>
- <Head>
- <title>OpenDC</title>
- </Head>
- <HomeNavbar />
- <div className="body-wrapper page-container">
- <JumbotronHeader />
- <IntroSection className={introSection} />
- <StakeholderSection className={stakeholderSection} />
- <ModelingSection className={modelingSection} />
- <SimulationSection className={simulationSection} />
- <TechnologiesSection className={technologiesSection} />
- <TeamSection className={teamSection} />
- <ContactSection />
- </div>
- </>
- )
-}
-
-export default Home
diff --git a/opendc-web/opendc-web-ui/src/pages/index.module.scss b/opendc-web/opendc-web-ui/src/pages/index.module.scss
deleted file mode 100644
index aed1d88f..00000000
--- a/opendc-web/opendc-web-ui/src/pages/index.module.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-.bodyWrapper {
- position: relative;
- overflow-y: hidden;
-}
-
-.introSection,
-.modelingSection,
-.technologiesSection {
- background-color: #fff;
-}
-
-.stakeholderSection,
-.simulationSection,
-.teamSection {
- background-color: #f2f2f2;
-}
diff --git a/opendc-web/opendc-web-ui/src/pages/logout.js b/opendc-web/opendc-web-ui/src/pages/logout.js
index e96e0605..38d5968e 100644
--- a/opendc-web/opendc-web-ui/src/pages/logout.js
+++ b/opendc-web/opendc-web-ui/src/pages/logout.js
@@ -22,17 +22,17 @@
import React from 'react'
import Head from 'next/head'
-import AppNavbarContainer from '../containers/navigation/AppNavbarContainer'
+import { AppPage } from '../components/AppPage'
+import { PageSection, PageSectionVariants } from '@patternfly/react-core'
function Logout() {
return (
- <>
+ <AppPage>
<Head>
<title>Logged Out - OpenDC</title>
</Head>
- <AppNavbarContainer fullWidth={false} />
- <span>Logged out successfully</span>
- </>
+ <PageSection variant={PageSectionVariants.light}>Logged out successfully</PageSection>
+ </AppPage>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
index cce887aa..c6ded12b 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js
@@ -20,6 +20,113 @@
* SOFTWARE.
*/
-import Topology from './topologies/[topology]'
+import { useRouter } from 'next/router'
+import { useProject } from '../../../data/project'
+import { AppPage } from '../../../components/AppPage'
+import Head from 'next/head'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ Button,
+ Card,
+ CardActions,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ Grid,
+ GridItem,
+ PageSection,
+ PageSectionVariants,
+ Skeleton,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+import BreadcrumbLink from '../../../components/util/BreadcrumbLink'
+import PortfolioTable from '../../../components/projects/PortfolioTable'
+import TopologyTable from '../../../components/projects/TopologyTable'
+import NewTopology from '../../../components/projects/NewTopology'
+import NewPortfolio from '../../../components/projects/NewPortfolio'
-export default Topology
+function Project() {
+ const router = useRouter()
+ const { project: projectId } = router.query
+
+ const { data: project } = useProject(projectId)
+
+ const breadcrumb = (
+ <Breadcrumb>
+ <BreadcrumbItem to="/projects" component={BreadcrumbLink}>
+ Projects
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}`} component={BreadcrumbLink} isActive>
+ Project details
+ </BreadcrumbItem>
+ </Breadcrumb>
+ )
+
+ return (
+ <AppPage breadcrumb={breadcrumb}>
+ <Head>
+ <title>{project?.name ?? 'Project'} - OpenDC</title>
+ </Head>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">
+ {project?.name ?? <Skeleton width="15%" screenreaderText="Loading project" />}
+ </Text>
+ </TextContent>
+ </PageSection>
+ <PageSection isFilled>
+ <Grid hasGutter>
+ <GridItem md={2}>
+ <Card>
+ <CardTitle>Details</CardTitle>
+ <CardBody>
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Name</DescriptionListTerm>
+ <DescriptionListDescription>
+ {project?.name ?? <Skeleton screenreaderText="Loading project" />}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ </CardBody>
+ </Card>
+ </GridItem>
+ <GridItem md={5}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <NewTopology projectId={projectId} />
+ </CardActions>
+ <CardTitle>Topologies</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <TopologyTable projectId={projectId} />
+ </CardBody>
+ </Card>
+ </GridItem>
+ <GridItem md={5}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <NewPortfolio projectId={projectId} />
+ </CardActions>
+ <CardTitle>Portfolios</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <PortfolioTable projectId={projectId} />
+ </CardBody>
+ </Card>
+ </GridItem>
+ </Grid>
+ </PageSection>
+ </AppPage>
+ )
+}
+
+export default Project
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
index d3d61271..55bee445 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js
@@ -22,12 +22,41 @@
import { useRouter } from 'next/router'
import Head from 'next/head'
-import AppNavbarContainer from '../../../../containers/navigation/AppNavbarContainer'
-import React from 'react'
-import { useProject } from '../../../../data/project'
-import ProjectSidebarContainer from '../../../../containers/app/sidebars/project/ProjectSidebarContainer'
-import PortfolioResultsContainer from '../../../../containers/app/results/PortfolioResultsContainer'
-import { useDispatch } from 'react-redux'
+import React, { useRef } from 'react'
+import { usePortfolio, useProject } from '../../../../data/project'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ Card,
+ CardActions,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ Chip,
+ ChipGroup,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ Divider,
+ Grid,
+ GridItem,
+ PageSection,
+ PageSectionVariants,
+ Skeleton,
+ Tab,
+ TabContent,
+ Tabs,
+ TabTitleText,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+import { AppPage } from '../../../../components/AppPage'
+import BreadcrumbLink from '../../../../components/util/BreadcrumbLink'
+import ScenarioTable from '../../../../components/projects/ScenarioTable'
+import NewScenario from '../../../../components/projects/NewScenario'
+import { METRIC_NAMES } from '../../../../util/available-metrics'
+import PortfolioResults from '../../../../components/app/results/PortfolioResults'
/**
* Page that displays the results in a portfolio.
@@ -36,24 +65,127 @@ function Portfolio() {
const router = useRouter()
const { project: projectId, portfolio: portfolioId } = router.query
- const project = useProject(projectId)
- const title = project?.name ? project?.name + ' - OpenDC' : 'Simulation - OpenDC'
+ const { data: project } = useProject(projectId)
+ const { data: portfolio } = usePortfolio(portfolioId)
- const dispatch = useDispatch()
+ const overviewRef = useRef(null)
+ const resultsRef = useRef(null)
+
+ const breadcrumb = (
+ <Breadcrumb>
+ <BreadcrumbItem to="/projects" component={BreadcrumbLink}>
+ Projects
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}`} component={BreadcrumbLink}>
+ Project details
+ </BreadcrumbItem>
+ <BreadcrumbItem to={`/projects/${projectId}/portfolios/${portfolioId}`} component={BreadcrumbLink} isActive>
+ Portfolio
+ </BreadcrumbItem>
+ </Breadcrumb>
+ )
return (
- <div className="page-container full-height">
+ <AppPage breadcrumb={breadcrumb}>
<Head>
- <title>{title}</title>
+ <title>{project?.name ?? 'Portfolios'} - OpenDC</title>
</Head>
- <AppNavbarContainer fullWidth={true} />
- <div className="full-height app-page-container">
- <ProjectSidebarContainer />
- <div className="container-fluid full-height">
- <PortfolioResultsContainer />
- </div>
- </div>
- </div>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">Portfolio</Text>
+ </TextContent>
+ </PageSection>
+ <PageSection type="none" variant={PageSectionVariants.light} className="pf-c-page__main-tabs" sticky="top">
+ <Divider component="div" />
+ <Tabs defaultActiveKey={0} className="pf-m-page-insets">
+ <Tab
+ eventKey={0}
+ title={<TabTitleText>Overview</TabTitleText>}
+ tabContentId="overview"
+ tabContentRef={overviewRef}
+ />
+ <Tab
+ eventKey={1}
+ title={<TabTitleText>Results</TabTitleText>}
+ tabContentId="results"
+ tabContentRef={resultsRef}
+ />
+ </Tabs>
+ </PageSection>
+ <PageSection isFilled>
+ <TabContent eventKey={0} id="overview" ref={overviewRef} aria-label="Overview tab">
+ <Grid hasGutter>
+ <GridItem md={2}>
+ <Card>
+ <CardTitle>Details</CardTitle>
+ <CardBody>
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Name</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.name ?? <Skeleton screenreaderText="Loading portfolio" />}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Scenarios</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.scenarioIds.length ?? (
+ <Skeleton screenreaderText="Loading portfolio" />
+ )}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Metrics</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.targets?.enabledMetrics ? (
+ portfolio.targets.enabledMetrics.length > 0 ? (
+ <ChipGroup>
+ {portfolio.targets.enabledMetrics.map((metric) => (
+ <Chip isReadOnly key={metric}>
+ {METRIC_NAMES[metric]}
+ </Chip>
+ ))}
+ </ChipGroup>
+ ) : (
+ 'No metrics enabled'
+ )
+ ) : (
+ <Skeleton screenreaderText="Loading portfolio" />
+ )}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>Repeats per Scenario</DescriptionListTerm>
+ <DescriptionListDescription>
+ {portfolio?.targets?.repeatsPerScenario ?? (
+ <Skeleton screenreaderText="Loading portfolio" />
+ )}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ </CardBody>
+ </Card>
+ </GridItem>
+ <GridItem md={6}>
+ <Card>
+ <CardHeader>
+ <CardActions>
+ <NewScenario portfolioId={portfolioId} />
+ </CardActions>
+ <CardTitle>Scenarios</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <ScenarioTable portfolioId={portfolioId} />
+ </CardBody>
+ </Card>
+ </GridItem>
+ </Grid>
+ </TabContent>
+ <TabContent eventKey={1} id="results" ref={resultsRef} aria-label="Results tab" hidden>
+ <PortfolioResults portfolioId={portfolioId} />
+ </TabContent>
+ </PageSection>
+ </AppPage>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
index a9dfdb19..5873ed11 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
@@ -23,18 +23,29 @@
import { useRouter } from 'next/router'
import { useProject } from '../../../../data/project'
import { useDispatch, useSelector } from 'react-redux'
-import React, { useEffect } from 'react'
+import React, { useEffect, useState } from 'react'
import { HotKeys } from 'react-hotkeys'
import { KeymapConfiguration } from '../../../../hotkeys'
import Head from 'next/head'
-import AppNavbarContainer from '../../../../containers/navigation/AppNavbarContainer'
-import LoadingScreen from '../../../../components/app/map/LoadingScreen'
-import MapStage from '../../../../containers/app/map/MapStage'
-import ScaleIndicatorContainer from '../../../../containers/app/map/controls/ScaleIndicatorContainer'
-import ToolPanelComponent from '../../../../components/app/map/controls/ToolPanelComponent'
-import ProjectSidebarContainer from '../../../../containers/app/sidebars/project/ProjectSidebarContainer'
-import TopologySidebarContainer from '../../../../containers/app/sidebars/topology/TopologySidebarContainer'
+import MapStage from '../../../../components/app/map/MapStage'
import { openProjectSucceeded } from '../../../../redux/actions/projects'
+import { AppPage } from '../../../../components/AppPage'
+import {
+ Bullseye,
+ Drawer,
+ DrawerContent,
+ DrawerContentBody,
+ EmptyState,
+ EmptyStateIcon,
+ Spinner,
+ Title,
+} from '@patternfly/react-core'
+import { zoomInOnCenter } from '../../../../redux/actions/map'
+import Toolbar from '../../../../components/app/map/controls/Toolbar'
+import { useMapScale } from '../../../../data/map'
+import ScaleIndicator from '../../../../components/app/map/controls/ScaleIndicator'
+import TopologySidebar from '../../../../components/app/sidebars/topology/TopologySidebar'
+import Collapse from '../../../../components/app/map/controls/Collapse'
/**
* Page that displays a datacenter topology.
@@ -44,7 +55,6 @@ function Topology() {
const { project: projectId, topology: topologyId } = router.query
const { data: project } = useProject(projectId)
- const title = project?.name ? project?.name + ' - OpenDC' : 'Simulation - OpenDC'
const dispatch = useDispatch()
useEffect(() => {
@@ -54,27 +64,44 @@ function Topology() {
}, [projectId, topologyId, dispatch])
const topologyIsLoading = useSelector((state) => state.currentTopologyId === '-1')
+ const scale = useMapScale()
+ const interactionLevel = useSelector((state) => state.interactionLevel)
+
+ const [isExpanded, setExpanded] = useState(true)
+ const panelContent = <TopologySidebar interactionLevel={interactionLevel} onClose={() => setExpanded(false)} />
return (
- <HotKeys keyMap={KeymapConfiguration} allowChanges={true} className="page-container full-height">
+ <AppPage>
<Head>
- <title>{title}</title>
+ <title>{project?.name ?? 'Topologies'} - OpenDC</title>
</Head>
- <AppNavbarContainer fullWidth={true} />
{topologyIsLoading ? (
- <div className="full-height d-flex align-items-center justify-content-center">
- <LoadingScreen />
- </div>
+ <Bullseye>
+ <EmptyState>
+ <EmptyStateIcon variant="container" component={Spinner} />
+ <Title size="lg" headingLevel="h4">
+ Loading Topology
+ </Title>
+ </EmptyState>
+ </Bullseye>
) : (
- <div className="full-height">
- <MapStage />
- <ScaleIndicatorContainer />
- <ToolPanelComponent />
- <ProjectSidebarContainer />
- <TopologySidebarContainer />
- </div>
+ <HotKeys keyMap={KeymapConfiguration} allowChanges={true} className="full-height">
+ <Drawer isExpanded={isExpanded}>
+ <DrawerContent panelContent={panelContent}>
+ <DrawerContentBody>
+ <MapStage />
+ <ScaleIndicator scale={scale} />
+ <Toolbar
+ onZoom={(zoomIn) => dispatch(zoomInOnCenter(zoomIn))}
+ onExport={() => window['exportCanvasToImage']()}
+ />
+ <Collapse onClick={() => setExpanded(true)} />
+ </DrawerContentBody>
+ </DrawerContent>
+ </Drawer>
+ </HotKeys>
)}
- </HotKeys>
+ </AppPage>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/projects/index.js b/opendc-web/opendc-web-ui/src/pages/projects/index.js
index 2d8e6de7..9dcc9aea 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/index.js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js
@@ -1,31 +1,87 @@
-import React, { useState } from 'react'
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { useMemo, useState } from 'react'
import Head from 'next/head'
import ProjectFilterPanel from '../../components/projects/FilterPanel'
-import NewProjectContainer from '../../containers/projects/NewProjectContainer'
-import ProjectListContainer from '../../containers/projects/ProjectListContainer'
-import AppNavbarContainer from '../../containers/navigation/AppNavbarContainer'
-import { useRequireAuth } from '../../auth'
-import { Container } from 'reactstrap'
+import { useAuth, useRequireAuth } from '../../auth'
+import { AppPage } from '../../components/AppPage'
+import { PageSection, PageSectionVariants, Text, TextContent } from '@patternfly/react-core'
+import { useProjects } from '../../data/project'
+import ProjectTable from '../../components/projects/ProjectTable'
+import { useMutation } from 'react-query'
+import NewProject from '../../components/projects/NewProject'
+
+const getVisibleProjects = (projects, filter, userId) => {
+ switch (filter) {
+ case 'SHOW_ALL':
+ return projects
+ case 'SHOW_OWN':
+ return projects.filter((project) =>
+ project.authorizations.some((a) => a.userId === userId && a.level === 'OWN')
+ )
+ case 'SHOW_SHARED':
+ return projects.filter((project) =>
+ project.authorizations.some((a) => a.userId === userId && a.level !== 'OWN')
+ )
+ default:
+ return projects
+ }
+}
function Projects() {
useRequireAuth()
-
+ const { user } = useAuth()
+ const { status, data: projects } = useProjects()
const [filter, setFilter] = useState('SHOW_ALL')
+ const visibleProjects = useMemo(() => getVisibleProjects(projects ?? [], filter, user?.sub), [
+ projects,
+ filter,
+ user?.sub,
+ ])
+
+ const { mutate: deleteProject } = useMutation('deleteProject')
return (
- <>
+ <AppPage>
<Head>
<title>My Projects - OpenDC</title>
</Head>
- <div className="full-height">
- <AppNavbarContainer fullWidth={false} />
- <Container className="text-page-container">
- <ProjectFilterPanel onSelect={setFilter} activeFilter={filter} />
- <ProjectListContainer filter={filter} />
- <NewProjectContainer />
- </Container>
- </div>
- </>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">My Projects</Text>
+ </TextContent>
+ </PageSection>
+ <PageSection variant={PageSectionVariants.light} isFilled>
+ <ProjectFilterPanel onSelect={setFilter} activeFilter={filter} />
+ <ProjectTable
+ status={status}
+ isFiltering={filter !== 'SHOW_ALL'}
+ projects={visibleProjects}
+ onDelete={(project) => deleteProject(project._id)}
+ />
+ <NewProject />
+ </PageSection>
+ </AppPage>
)
}
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
index 5d9154fd..333c1485 100644
--- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
+++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
@@ -123,8 +123,6 @@ 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) {
@@ -148,8 +146,6 @@ export function* onEditRackName(action) {
try {
const topologyId = yield select((state) => state.currentTopologyId)
const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rack)
- 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) {
@@ -175,6 +171,7 @@ export function* onAddRackToTile(action) {
const rack = {
_id: uuid(),
name: 'Rack',
+ tileId: action.tileId,
capacity: DEFAULT_RACK_SLOT_CAPACITY,
powerCapacityW: DEFAULT_RACK_POWER_CAPACITY,
machines: [],
@@ -195,6 +192,7 @@ export function* onAddMachine(action) {
const machine = {
_id: uuid(),
+ rackId,
position: action.position,
cpus: [],
gpus: [],
diff --git a/opendc-web/opendc-web-ui/src/shapes.js b/opendc-web/opendc-web-ui/src/shapes.js
index 3c27ad11..abdf146e 100644
--- a/opendc-web/opendc-web-ui/src/shapes.js
+++ b/opendc-web/opendc-web-ui/src/shapes.js
@@ -149,3 +149,5 @@ export const InteractionLevel = PropTypes.shape({
roomId: PropTypes.string,
rackId: PropTypes.string,
})
+
+export const Status = PropTypes.oneOf(['idle', 'loading', 'error', 'success'])
diff --git a/opendc-web/opendc-web-ui/src/style/_mixins.scss b/opendc-web/opendc-web-ui/src/style/_mixins.scss
deleted file mode 100644
index 5f103cd7..00000000
--- a/opendc-web/opendc-web-ui/src/style/_mixins.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-/* General Button Abstractions */
-@mixin clickable {
- cursor: pointer;
- user-select: none;
-}
diff --git a/opendc-web/opendc-web-ui/src/style/_variables.scss b/opendc-web/opendc-web-ui/src/style/_variables.scss
deleted file mode 100644
index e3df6cbd..00000000
--- a/opendc-web/opendc-web-ui/src/style/_variables.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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/style/index.scss b/opendc-web/opendc-web-ui/src/style/index.scss
new file mode 100644
index 00000000..ff84e24a
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/style/index.scss
@@ -0,0 +1,36 @@
+/*!
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+body,
+#__next {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+
+ background: #eee;
+}
+
+.full-height {
+ position: relative;
+ height: 100% !important;
+}
diff --git a/opendc-web/opendc-web-ui/src/util/authorizations.js b/opendc-web/opendc-web-ui/src/util/authorizations.js
index eb492d26..ce5d34b6 100644
--- a/opendc-web/opendc-web-ui/src/util/authorizations.js
+++ b/opendc-web/opendc-web-ui/src/util/authorizations.js
@@ -1,9 +1,11 @@
-import { faHome, faPencilAlt, faEye } from '@fortawesome/free-solid-svg-icons'
+import HomeIcon from '@patternfly/react-icons/dist/js/icons/home-icon'
+import EditIcon from '@patternfly/react-icons/dist/js/icons/edit-icon'
+import EyeIcon from '@patternfly/react-icons/dist/js/icons/eye-icon'
export const AUTH_ICON_MAP = {
- OWN: faHome,
- EDIT: faPencilAlt,
- VIEW: faEye,
+ OWN: HomeIcon,
+ EDIT: EditIcon,
+ VIEW: EyeIcon,
}
export const AUTH_DESCRIPTION_MAP = {
diff --git a/opendc-web/opendc-web-ui/src/util/available-metrics.js b/opendc-web/opendc-web-ui/src/util/available-metrics.js
index 807bc0c1..b21ab150 100644
--- a/opendc-web/opendc-web-ui/src/util/available-metrics.js
+++ b/opendc-web/opendc-web-ui/src/util/available-metrics.js
@@ -1,12 +1,28 @@
+export const METRIC_GROUPS = {
+ 'Host 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',
+ ],
+ 'Compute Service Metrics': ['total_vms_submitted', 'total_vms_queued', 'total_vms_finished', 'total_vms_failed'],
+}
+
export const AVAILABLE_METRICS = [
+ 'mean_cpu_usage',
+ 'mean_cpu_demand',
+ 'total_requested_burst',
+ 'total_granted_burst',
'total_overcommitted_burst',
+ 'total_interfered_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',
@@ -65,3 +81,22 @@ export const METRIC_UNITS = {
total_vms_finished: 'VMs',
total_vms_failed: 'VMs',
}
+
+export const METRIC_DESCRIPTIONS = {
+ total_overcommitted_burst:
+ 'The total CPU clock cycles lost due to overcommitting of resources. This metric is an indicator for resource overload.',
+ total_requested_burst: 'The total CPU clock cycles that were requested by all virtual machines.',
+ total_granted_burst: 'The total CPU clock cycles executed by the hosts.',
+ total_interfered_burst: 'The total CPU clock cycles lost due to resource interference between virtual machines.',
+ total_power_draw: 'The average power usage in watts.',
+ mean_cpu_usage: 'The average amount of CPU clock cycles consumed by all virtual machines on a host.',
+ mean_cpu_demand: 'The average amount of CPU clock cycles requested by all powered on virtual machines on a host.',
+ mean_num_deployed_images: 'The average number of virtual machines deployed on a host.',
+ max_num_deployed_images: 'The maximum number of virtual machines deployed at any time.',
+ total_failure_vm_slices: 'The total amount of CPU clock cycles lost due to failure.',
+ total_vms_submitted: 'The total number of virtual machines scheduled by the compute service.',
+ total_vms_queued:
+ 'The maximum number of virtual machines waiting to be scheduled by the compute service at any point.',
+ total_vms_finished: 'The total number of virtual machines that completed successfully.',
+ total_vms_failed: 'The total number of virtual machines that failed during execution.',
+}
diff --git a/opendc-web/opendc-web-ui/src/util/date-time.js b/opendc-web/opendc-web-ui/src/util/date-time.js
index 66efdf5b..7e2f6623 100644
--- a/opendc-web/opendc-web-ui/src/util/date-time.js
+++ b/opendc-web/opendc-web-ui/src/util/date-time.js
@@ -7,19 +7,7 @@
* @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')
+ return formatDateTime(new Date(dateTimeString))
}
/**
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
index 3d95eba6..431e39f7 100644
--- a/opendc-web/opendc-web-ui/src/util/date-time.test.js
+++ b/opendc-web/opendc-web-ui/src/util/date-time.test.js
@@ -1,18 +1,4 @@
-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)
- })
-})
+import { convertSecondsToFormattedTime } from './date-time'
describe('tick formatting', () => {
it("returns '0s' for numbers <= 0", () => {
diff --git a/opendc-web/opendc-web-ui/yarn.lock b/opendc-web/opendc-web-ui/yarn.lock
index bf4c419e..cd446e99 100644
--- a/opendc-web/opendc-web-ui/yarn.lock
+++ b/opendc-web/opendc-web-ui/yarn.lock
@@ -148,14 +148,6 @@
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==
-"@hypnosphi/create-react-context@^0.3.1":
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz#f8bfebdc7665f5d426cba3753e0e9c7d3154d7c6"
- integrity sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==
- dependencies:
- gud "^1.0.0"
- warning "^4.0.3"
-
"@next/env@11.0.1":
version "11.0.1"
resolved "https://registry.yarnpkg.com/@next/env/-/env-11.0.1.tgz#6dc96ac76f1663ab747340e907e8933f190cc8fd"
@@ -214,6 +206,46 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@patternfly/react-core@^4.135.7":
+ version "4.135.7"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-4.135.7.tgz#7292e4472ffcd519e3db1af782cb4d28024a6992"
+ integrity sha512-bxYLJ7TTXiEVHscfzqGbI+YFqwQMYWfG2Yknw/b/QqwN/bv5eCpioYOo35Wr4jvFAWkXvUUKp/DUpcSHrrFgTg==
+ dependencies:
+ "@patternfly/react-icons" "^4.11.2"
+ "@patternfly/react-styles" "^4.11.2"
+ "@patternfly/react-tokens" "^4.12.3"
+ focus-trap "6.2.2"
+ react-dropzone "9.0.0"
+ tippy.js "5.1.2"
+ tslib "1.13.0"
+
+"@patternfly/react-icons@^4.11.2":
+ version "4.11.2"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.11.2.tgz#46f01197f779996954fd8033681644dd167cebcb"
+ integrity sha512-BPRWHAopry4a8aFd3VWaqO8h7QDA4NeYZ80YAY1iGqWeClIAmZlxs6jGCUhTC7iuYmjKruiVoDQwuwr/u+p6JQ==
+
+"@patternfly/react-styles@^4.11.2":
+ version "4.11.2"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.11.2.tgz#3548d1c2a5b8a989f9edc6bd31165952e5f8bcb7"
+ integrity sha512-rUtw2tfniBiVIYEXPO8buisWFEHVR6dO5PH6C2MvbMk+VyUIS31TzOeNFyJs/gCurt7t1dEKjI9yNEGuAOjUPQ==
+
+"@patternfly/react-table@^4.29.8":
+ version "4.29.8"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-4.29.8.tgz#9a8f1d1332c2f708427c81a693c2e90f9f52c75e"
+ integrity sha512-N6oKSE0sRHvkc9KWF03oTISfsc6G5NRBRCedi81hnpOSVo2515lGnA/r1ipR1DwM9qHYxmPNsrkPeoYtDQBOSQ==
+ dependencies:
+ "@patternfly/react-core" "^4.135.7"
+ "@patternfly/react-icons" "^4.11.2"
+ "@patternfly/react-styles" "^4.11.2"
+ "@patternfly/react-tokens" "^4.12.3"
+ lodash "^4.17.19"
+ tslib "1.13.0"
+
+"@patternfly/react-tokens@^4.12.3":
+ version "4.12.3"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-4.12.3.tgz#600b77e460e291032d649febf43002fe355a4468"
+ integrity sha512-RgXPEDdgx/p2VPQyCCxvv+ff9lmgYm4QDcI3c8iIwQjpPnJ5KF/bT6lPoAfeLFocRIZDnDSDcwSINbQwPQGp2A==
+
"@redux-saga/core@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.1.3.tgz#3085097b57a4ea8db5528d58673f20ce0950f6a4"
@@ -645,6 +677,13 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
+attr-accept@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
+ integrity sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==
+ dependencies:
+ core-js "^2.5.0"
+
available-typed-arrays@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
@@ -692,11 +731,6 @@ bn.js@^5.0.0, bn.js@^5.1.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==
-bootstrap@~4.6.0:
- version "4.6.0"
- resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7"
- integrity sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==
-
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -923,7 +957,7 @@ classnames@2.2.6, classnames@~2.2.5:
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
-classnames@^2.2.3, classnames@^2.2.5:
+classnames@^2.2.5:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
@@ -1024,6 +1058,11 @@ convert-source-map@1.7.0:
dependencies:
safe-buffer "~5.1.1"
+core-js@^2.5.0:
+ version "2.6.12"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
+ integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
+
core-js@^3.11.0:
version "3.12.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.12.1.tgz#6b5af4ff55616c08a44d386f1f510917ff204112"
@@ -1249,18 +1288,6 @@ deep-diff@^0.3.5:
resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84"
integrity sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=
-deep-equal@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
- integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
- dependencies:
- is-arguments "^1.0.4"
- is-date-object "^1.0.1"
- is-regex "^1.0.4"
- object-is "^1.0.1"
- object-keys "^1.1.1"
- regexp.prototype.flags "^1.2.0"
-
deep-is@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
@@ -1380,6 +1407,14 @@ end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
+enhanced-resolve@^5.7.0:
+ version "5.8.2"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b"
+ integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==
+ dependencies:
+ graceful-fs "^4.2.4"
+ tapable "^2.2.0"
+
enquirer@^2.3.5, enquirer@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
@@ -1764,6 +1799,13 @@ file-saver@^1.3.3:
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8"
integrity sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg==
+file-selector@^0.1.8:
+ version "0.1.19"
+ resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.19.tgz#8ecc9d069a6f544f2e4a096b64a8052e70ec8abf"
+ integrity sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==
+ dependencies:
+ tslib "^2.0.1"
+
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -1815,6 +1857,13 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
+focus-trap@6.2.2:
+ version "6.2.2"
+ resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-6.2.2.tgz#0e6f391415b0697c99da932702dedd13084fa131"
+ integrity sha512-qWovH9+LGoKqREvJaTCzJyO0hphQYGz+ap5Hc4NqXHNhZBdxCi5uBPPcaOUw66fHmzXLVwvETLvFgpwPILqKpg==
+ dependencies:
+ tabbable "^5.1.4"
+
foreach@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
@@ -1923,16 +1972,11 @@ globby@^11.0.3:
merge2 "^1.3.0"
slash "^3.0.0"
-graceful-fs@^4.1.2:
+graceful-fs@^4.1.2, graceful-fs@^4.2.4:
version "4.2.6"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
-gud@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
- integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
-
has-bigints@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
@@ -2229,7 +2273,7 @@ is-obj@^1.0.1:
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
-is-regex@^1.0.4, is-regex@^1.1.2, is-regex@^1.1.3:
+is-regex@^1.1.2, is-regex@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
@@ -2646,6 +2690,14 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+next-transpile-modules@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-8.0.0.tgz#56375cdc25ae5d23a834195f277fc2737b26cb97"
+ integrity sha512-Q2f2yB0zMJ8KJbIYAeZoIxG6cSfVk813zr6B5HzsLMBVcJ3FaF8lKr7WG66n0KlHCwjLSmf/6EkgI6QQVWHrDw==
+ dependencies:
+ enhanced-resolve "^5.7.0"
+ escalade "^3.1.1"
+
next@^11.0.1:
version "11.0.1"
resolved "https://registry.yarnpkg.com/next/-/next-11.0.1.tgz#b8e3914d153aaf7143cb98c09bcd3c8230eeb17a"
@@ -3083,7 +3135,7 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
-popper.js@^1.14.4:
+popper.js@^1.16.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
@@ -3132,7 +3184,15 @@ promise-polyfill@^8.2.0:
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0"
integrity sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==
-prop-types@15.7.2, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@~15.7.2:
+prop-types-extra@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b"
+ integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==
+ dependencies:
+ react-is "^16.3.2"
+ warning "^4.0.0"
+
+prop-types@15.7.2, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@~15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -3244,6 +3304,16 @@ react-dom@^17.0.2:
object-assign "^4.1.1"
scheduler "^0.20.2"
+react-dropzone@9.0.0:
+ version "9.0.0"
+ resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-9.0.0.tgz#4f5223cdcb4d3bd8a66e3298c4041eb0c75c4634"
+ integrity sha512-wZ2o9B2qkdE3RumWhfyZT9swgJYJPeU5qHEcMU8weYpmLex1eeWX0CC32/Y0VutB+BBi2D+iePV/YZIiB4kZGw==
+ dependencies:
+ attr-accept "^1.1.3"
+ file-selector "^0.1.8"
+ prop-types "^15.6.2"
+ prop-types-extra "^1.1.0"
+
react-hotkeys@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-2.0.0.tgz#a7719c7340cbba888b0e9184f806a9ec0ac2c53f"
@@ -3261,7 +3331,7 @@ react-is@17.0.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
-react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
+react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -3279,19 +3349,6 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
-react-popper@^1.3.6:
- version "1.3.11"
- resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.11.tgz#a2cc3f0a67b75b66cfa62d2c409f9dd1fcc71ffd"
- integrity sha512-VSA/bS+pSndSF2fiasHK/PTEEAyOpX60+H5EPAjoArr8JGm+oihu4UbrqcEBpQibJxBVCpYyjAX7abJ+7DoYVg==
- dependencies:
- "@babel/runtime" "^7.1.2"
- "@hypnosphi/create-react-context" "^0.3.1"
- deep-equal "^1.1.1"
- popper.js "^1.14.4"
- prop-types "^15.6.1"
- typed-styles "^0.0.7"
- warning "^4.0.2"
-
react-query@^3.18.1:
version "3.18.1"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.18.1.tgz#893b5475a7b4add099e007105317446f7a2cd310"
@@ -3346,7 +3403,7 @@ react-smooth@^2.0.0:
raf "^3.4.0"
react-transition-group "2.9.0"
-react-transition-group@2.9.0, react-transition-group@^2.3.1:
+react-transition-group@2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
@@ -3364,17 +3421,6 @@ react@^17.0.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
-reactstrap@^8.9.0:
- version "8.9.0"
- resolved "https://registry.yarnpkg.com/reactstrap/-/reactstrap-8.9.0.tgz#bca4afa3f5cd18899ef9b33d877a141886d5abae"
- integrity sha512-pmf33YjpNZk1IfrjqpWCUMq9hk6GzSnMWBAofTBNIRJQB1zQ0Au2kzv3lPUAFsBYgWEuI9iYa/xKXHaboSiMkQ==
- dependencies:
- "@babel/runtime" "^7.12.5"
- classnames "^2.2.3"
- prop-types "^15.5.8"
- react-popper "^1.3.6"
- react-transition-group "^2.3.1"
-
read-pkg-up@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
@@ -3494,7 +3540,7 @@ regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
-regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.1:
+regexp.prototype.flags@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26"
integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==
@@ -3975,6 +4021,11 @@ symbol-observable@^1.2.0:
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+tabbable@^5.1.4:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.2.0.tgz#4fba60991d8bb89d06e5d9455c92b453acf88fb2"
+ integrity sha512-0uyt8wbP0P3T4rrsfYg/5Rg3cIJ8Shl1RJ54QMqYxm1TLdWqJD1u6+RQjr2Lor3wmfT7JRHkirIwy99ydBsyPg==
+
table@^6.0.9:
version "6.7.1"
resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2"
@@ -3987,6 +4038,11 @@ table@^6.0.9:
string-width "^4.2.0"
strip-ansi "^6.0.0"
+tapable@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
+ integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==
+
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -4009,6 +4065,13 @@ tiny-emitter@^2.1.0:
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
+tippy.js@5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-5.1.2.tgz#5ac91233c59ab482ef5988cffe6e08bd26528e66"
+ integrity sha512-Qtrv2wqbRbaKMUb6bWWBQWPayvcDKNrGlvihxtsyowhT7RLGEh1STWuy6EMXC6QLkfKPB2MLnf8W2mzql9VDAw==
+ dependencies:
+ popper.js "^1.16.0"
+
to-arraybuffer@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
@@ -4053,11 +4116,21 @@ tsconfig-paths@^3.9.0:
minimist "^1.2.0"
strip-bom "^3.0.0"
+tslib@1.13.0:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
+ integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
+
tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+tslib@^2.0.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
+ integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
+
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
@@ -4107,11 +4180,6 @@ typed-function@^2.0.0:
resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-2.0.0.tgz#15ab3825845138a8b1113bd89e60cd6a435739e8"
integrity sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA==
-typed-styles@^0.0.7:
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
- integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==
-
typescript-compare@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/typescript-compare/-/typescript-compare-0.0.2.tgz#7ee40a400a406c2ea0a7e551efd3309021d5f425"
@@ -4174,6 +4242,13 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"
+use-resize-observer@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-7.0.0.tgz#15f0efbd5a4e08a8cc51901f21a89ba836f2116e"
+ integrity sha512-+RjrQsk/mL8aKy4TGBDiPkUv6whyeoGDMIZYk0gOGHOlnrsjImC+jG6lfAFcBCKAG9epGRL419adhDNdkDCQkA==
+ dependencies:
+ resize-observer-polyfill "^1.5.1"
+
use-subscription@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
@@ -4243,7 +4318,7 @@ vm-browserify@1.1.2, vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
-warning@^4.0.2, warning@^4.0.3:
+warning@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==