summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web/opendc-web-ui/src')
-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
187 files changed, 4246 insertions, 3063 deletions
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", () => {