summaryrefslogtreecommitdiff
path: root/opendc-web
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-07-14 22:23:40 +0200
committerFabian Mastenbroek <mail.fabianm@gmail.com>2021-07-15 15:55:56 +0200
commit803e13b32cf0ff8b496649fb0a4d6e32400e98a4 (patch)
tree263a6f9741c5ca0dd64ecf3f7f07b580331aec9d /opendc-web
parente200dbfdc076ac6263c9ac6f9dabdcc475f01d6e (diff)
feat(ui): Migrate to PatternFly 4 design framework
This change is a rewrite of the existing OpenDC frontend in order to migrate to the PatternFly 4 design framework. PatternFly is used by Red Hat for various computing related services such as OpenShift, Red Hat Virtualization and Cockpit. Since their design requirements are very similar to those of OpenDC (modeling computing services), migrating to PatternFly 4 allows us to re-use design choices from these services. See https://www.patternfly.org/v4/ for more information about PatternFly.
Diffstat (limited to 'opendc-web')
-rw-r--r--opendc-web/opendc-web-ui/next.config.js2
-rw-r--r--opendc-web/opendc-web-ui/package.json7
-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/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/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.js15
-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/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.lock132
190 files changed, 4298 insertions, 3152 deletions
diff --git a/opendc-web/opendc-web-ui/next.config.js b/opendc-web/opendc-web-ui/next.config.js
index 56263c86..9092adc4 100644
--- a/opendc-web/opendc-web-ui/next.config.js
+++ b/opendc-web/opendc-web-ui/next.config.js
@@ -25,6 +25,8 @@
const withTM = require('next-transpile-modules')([
'@patternfly/react-core',
'@patternfly/react-styles',
+ '@patternfly/react-table',
+ '@patternfly/react-tokens',
])
module.exports = withTM({
diff --git a/opendc-web/opendc-web-ui/package.json b/opendc-web/opendc-web-ui/package.json
index 442d63a5..84db4277 100644
--- a/opendc-web/opendc-web-ui/package.json
+++ b/opendc-web/opendc-web-ui/package.json
@@ -22,11 +22,12 @@
"@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.0",
+ "@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",
@@ -43,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",
@@ -51,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/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/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/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 78402323..028dab10 100644
--- a/opendc-web/opendc-web-ui/src/pages/_app.js
+++ b/opendc-web/opendc-web-ui/src/pages/_app.js
@@ -24,8 +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 '@patternfly/react-core/dist/styles/base.css'
import { AuthProvider, useAuth } from '../auth'
import * as Sentry from '@sentry/react'
import { Integrations } from '@sentry/tracing'
@@ -35,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/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/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 abd2613e..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,33 +206,45 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
-"@patternfly/react-core@^4.135.0":
- version "4.135.0"
- resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-4.135.0.tgz#b64ad4da10a8814926e28fad727bc7690cd60e66"
- integrity sha512-DZcONUGOR7Znd6BsUJ4L+KrrnIpyjUvh3JNcYiHW3loytxShCGcx+a04QjOOcZm+MtFhkgs/t51yiC5IP12abA==
+"@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.0"
- "@patternfly/react-styles" "^4.11.0"
- "@patternfly/react-tokens" "^4.12.0"
+ "@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.0":
- version "4.11.0"
- resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.11.0.tgz#26790eeff22dc3204aa8cd094470f0a2f915634a"
- integrity sha512-WsIX34bO9rhVRmPG0jlV3GoFGfYgPC64TscNV0lxQosiVRnYIA6Z3nBSArtJsxo5Yn6c63glIefC/YTy6D/ZYg==
-
-"@patternfly/react-styles@^4.11.0":
- version "4.11.0"
- resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.11.0.tgz#0068dcb18e1343242f93fa6024dcc077acd57fb9"
- integrity sha512-4eIqTwGI4mjt9DMqX6hnan4eRS+3LUWNaneTEJdmk+flKxtAE/O/OmQHvH4GetDnlSbyfATcA0VFbVtR0aRJAg==
+"@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.0":
- version "4.12.0"
- resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-4.12.0.tgz#2973c7f08a2f35997a0054bbf3c886b3c5c68822"
- integrity sha512-Oj+GxqTtx0Yu9IDCTibZLQnpcKp58JneNKEFQkJ29WJydhPG4j6oFFElkK+ub+Ft/f9B1Ky1SsfR9eabo6IykQ==
+"@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"
@@ -727,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"
@@ -958,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==
@@ -1289,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"
@@ -1990,11 +1977,6 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4:
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"
@@ -2291,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==
@@ -3153,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:
+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==
@@ -3210,7 +3192,7 @@ prop-types-extra@^1.1.0:
react-is "^16.3.2"
warning "^4.0.0"
-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@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==
@@ -3367,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"
@@ -3434,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==
@@ -3452,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"
@@ -3582,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==
@@ -4222,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"
@@ -4289,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"
@@ -4358,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.0, 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==