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/api/portfolios.js12
-rw-r--r--opendc-web/opendc-web-ui/src/api/projects.js4
-rw-r--r--opendc-web/opendc-web-ui/src/api/scenarios.js12
-rw-r--r--opendc-web/opendc-web-ui/src/api/schedulers.js2
-rw-r--r--opendc-web/opendc-web-ui/src/api/topologies.js14
-rw-r--r--opendc-web/opendc-web-ui/src/api/traces.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js9
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js8
-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.js6
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js4
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js38
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js22
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js10
-rw-r--r--opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js19
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/App.js111
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js2
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js5
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js2
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js2
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js4
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js6
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js29
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js35
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js63
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js24
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js2
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js14
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js33
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/EmptySlotContainer.js12
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js14
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js27
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js2
-rw-r--r--opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js2
-rw-r--r--opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js5
-rw-r--r--opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js7
-rw-r--r--opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js7
-rw-r--r--opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js4
-rw-r--r--opendc-web/opendc-web-ui/src/data/experiments.js16
-rw-r--r--opendc-web/opendc-web-ui/src/data/project.js149
-rw-r--r--opendc-web/opendc-web-ui/src/data/topology.js55
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_app.js23
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_document.js2
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/index.js16
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/portfolios/[portfolio].js39
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js81
-rw-r--r--opendc-web/opendc-web-ui/src/pages/projects/index.js7
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/portfolios.js41
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/projects.js47
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/scenarios.js43
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/topologies.js11
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/topology/building.js13
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/topology/room.js2
-rw-r--r--opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js6
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js6
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js44
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/index.js7
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js6
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/objects.js20
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/projects.js14
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/index.js26
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/objects.js238
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js136
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js11
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/projects.js44
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js69
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/topology.js126
-rw-r--r--opendc-web/opendc-web-ui/src/shapes.js31
-rw-r--r--opendc-web/opendc-web-ui/src/util/topology-schema.js47
71 files changed, 674 insertions, 1292 deletions
diff --git a/opendc-web/opendc-web-ui/src/api/portfolios.js b/opendc-web/opendc-web-ui/src/api/portfolios.js
index 28898e6a..82ac0ced 100644
--- a/opendc-web/opendc-web-ui/src/api/portfolios.js
+++ b/opendc-web/opendc-web-ui/src/api/portfolios.js
@@ -22,12 +22,16 @@
import { request } from './index'
-export function addPortfolio(auth, projectId, portfolio) {
- return request(auth, `projects/${projectId}/portfolios`, 'POST', { portfolio })
+export function fetchPortfolio(auth, portfolioId) {
+ return request(auth, `portfolios/${portfolioId}`)
}
-export function getPortfolio(auth, portfolioId) {
- return request(auth, `portfolios/${portfolioId}`)
+export function fetchPortfoliosOfProject(auth, projectId) {
+ return request(auth, `projects/${projectId}/portfolios`)
+}
+
+export function addPortfolio(auth, portfolio) {
+ return request(auth, `projects/${portfolio.projectId}/portfolios`, 'POST', { portfolio })
}
export function updatePortfolio(auth, portfolioId, portfolio) {
diff --git a/opendc-web/opendc-web-ui/src/api/projects.js b/opendc-web/opendc-web-ui/src/api/projects.js
index 93052080..4123b371 100644
--- a/opendc-web/opendc-web-ui/src/api/projects.js
+++ b/opendc-web/opendc-web-ui/src/api/projects.js
@@ -22,11 +22,11 @@
import { request } from './index'
-export function getProjects(auth) {
+export function fetchProjects(auth) {
return request(auth, `projects/`)
}
-export function getProject(auth, projectId) {
+export function fetchProject(auth, projectId) {
return request(auth, `projects/${projectId}`)
}
diff --git a/opendc-web/opendc-web-ui/src/api/scenarios.js b/opendc-web/opendc-web-ui/src/api/scenarios.js
index 095aa788..88516caa 100644
--- a/opendc-web/opendc-web-ui/src/api/scenarios.js
+++ b/opendc-web/opendc-web-ui/src/api/scenarios.js
@@ -22,12 +22,16 @@
import { request } from './index'
-export function addScenario(auth, portfolioId, scenario) {
- return request(auth, `portfolios/${portfolioId}/scenarios`, 'POST', { scenario })
+export function fetchScenario(auth, scenarioId) {
+ return request(auth, `scenarios/${scenarioId}`)
}
-export function getScenario(auth, scenarioId) {
- return request(auth, `scenarios/${scenarioId}`)
+export function fetchScenariosOfPortfolio(auth, portfolioId) {
+ return request(auth, `portfolios/${portfolioId}/scenarios`)
+}
+
+export function addScenario(auth, scenario) {
+ return request(auth, `portfolios/${scenario.portfolioId}/scenarios`, 'POST', { scenario })
}
export function updateScenario(auth, scenarioId, scenario) {
diff --git a/opendc-web/opendc-web-ui/src/api/schedulers.js b/opendc-web/opendc-web-ui/src/api/schedulers.js
index 1b69f1a1..0b8b8153 100644
--- a/opendc-web/opendc-web-ui/src/api/schedulers.js
+++ b/opendc-web/opendc-web-ui/src/api/schedulers.js
@@ -22,6 +22,6 @@
import { request } from './index'
-export function getAllSchedulers(auth) {
+export function fetchSchedulers(auth) {
return request(auth, 'schedulers/')
}
diff --git a/opendc-web/opendc-web-ui/src/api/topologies.js b/opendc-web/opendc-web-ui/src/api/topologies.js
index 802be4bb..0b8864e0 100644
--- a/opendc-web/opendc-web-ui/src/api/topologies.js
+++ b/opendc-web/opendc-web-ui/src/api/topologies.js
@@ -22,16 +22,20 @@
import { request } from './index'
-export function addTopology(auth, topology) {
- return request(auth, `projects/${topology.projectId}/topologies`, 'POST', { topology })
+export function fetchTopology(auth, topologyId) {
+ return request(auth, `topologies/${topologyId}`)
}
-export function getTopology(auth, topologyId) {
- return request(auth, `topologies/${topologyId}`)
+export function fetchTopologiesOfProject(auth, projectId) {
+ return request(auth, `projects/${projectId}/topologies`)
+}
+
+export function addTopology(auth, topology) {
+ return request(auth, `projects/${topology.projectId}/topologies`, 'POST', { topology })
}
export function updateTopology(auth, topology) {
- const { _id, ...data } = topology;
+ const { _id, ...data } = topology
return request(auth, `topologies/${topology._id}`, 'PUT', { topology: data })
}
diff --git a/opendc-web/opendc-web-ui/src/api/traces.js b/opendc-web/opendc-web-ui/src/api/traces.js
index df03a2dd..fd637ac3 100644
--- a/opendc-web/opendc-web-ui/src/api/traces.js
+++ b/opendc-web/opendc-web-ui/src/api/traces.js
@@ -22,6 +22,6 @@
import { request } from './index'
-export function getAllTraces(auth) {
+export function fetchTraces(auth) {
return request(auth, 'traces/')
}
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
index 07b2d8f0..c3177fe1 100644
--- a/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js
@@ -37,10 +37,11 @@ function MapStageComponent({
setPos([mousePos.x, mousePos.y])
}
- useEffect(() => {
- const updateDimensions = () => setMapDimensions(window.innerWidth, window.innerHeight - NAVBAR_HEIGHT)
- const updateScale = (e) => zoomInOnPosition(e.deltaY < 0, x, 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)
@@ -57,7 +58,7 @@ function MapStageComponent({
window.removeEventListener('resize', updateDimensions)
window.removeEventListener('wheel', updateScale)
}
- }, [x, y, setMapDimensions, zoomInOnPosition])
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
const store = useStore()
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 e67d54fc..42d20ff1 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
@@ -10,7 +10,7 @@ const RoomGroup = ({ room, interactionLevel, currentRoomInConstruction, onClick
if (currentRoomInConstruction === room._id) {
return (
<Group onClick={onClick}>
- {room.tileIds.map((tileId) => (
+ {room.tiles.map((tileId) => (
<TileContainer key={tileId} tileId={tileId} newTile={true} />
))}
</Group>
@@ -25,16 +25,16 @@ const RoomGroup = ({ room, interactionLevel, currentRoomInConstruction, onClick
interactionLevel.roomId === room._id
) {
return [
- room.tileIds
+ room.tiles
.filter((tileId) => tileId !== interactionLevel.tileId)
.map((tileId) => <TileContainer key={tileId} tileId={tileId} />),
<GrayContainer key={-1} />,
- room.tileIds
+ room.tiles
.filter((tileId) => tileId === interactionLevel.tileId)
.map((tileId) => <TileContainer key={tileId} tileId={tileId} />),
]
} else {
- return room.tileIds.map((tileId) => <TileContainer key={tileId} tileId={tileId} />)
+ return room.tiles.map((tileId) => <TileContainer key={tileId} tileId={tileId} />)
}
})()}
<WallContainer roomId={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 2a108b93..ce5e4a6b 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
@@ -8,7 +8,7 @@ import RoomTile from '../elements/RoomTile'
const TileGroup = ({ tile, newTile, onClick }) => {
let tileObject
- if (tile.rackId) {
+ if (tile.rack) {
tileObject = <RackContainer tile={tile} />
} else {
tileObject = null
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 57107768..d4c6db7d 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
@@ -12,7 +12,7 @@ const TopologyGroup = ({ topology, interactionLevel }) => {
if (interactionLevel.mode === 'BUILDING') {
return (
<Group>
- {topology.roomIds.map((roomId) => (
+ {topology.rooms.map((roomId) => (
<RoomContainer key={roomId} roomId={roomId} />
))}
</Group>
@@ -21,13 +21,13 @@ const TopologyGroup = ({ topology, interactionLevel }) => {
return (
<Group>
- {topology.roomIds
+ {topology.rooms
.filter((roomId) => roomId !== interactionLevel.roomId)
.map((roomId) => (
<RoomContainer key={roomId} roomId={roomId} />
))}
{interactionLevel.mode === 'ROOM' ? <GrayContainer /> : null}
- {topology.roomIds
+ {topology.rooms
.filter((roomId) => roomId === interactionLevel.roomId)
.map((roomId) => (
<RoomContainer key={roomId} roomId={roomId} />
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
index ce271819..d61ff24e 100644
--- 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
@@ -25,7 +25,7 @@ function PortfolioListComponent({
</Button>
</h2>
- {portfolios.map((portfolio, idx) => (
+ {portfolios.map((portfolio) => (
<div key={portfolio._id}>
<Row className="row mb-1">
<Col
@@ -61,7 +61,7 @@ function PortfolioListComponent({
PortfolioListComponent.propTypes = {
portfolios: PropTypes.arrayOf(Portfolio),
- currentProjectId: PropTypes.string.isRequired,
+ currentProjectId: PropTypes.string,
currentPortfolioId: PropTypes.string,
onNewPortfolio: PropTypes.func.isRequired,
onChoosePortfolio: PropTypes.func.isRequired,
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
index f990dfcb..e81d2b78 100644
--- 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
@@ -1,48 +1,19 @@
import PropTypes from 'prop-types'
import React from 'react'
import { Scenario } from '../../../../shapes'
-import Link from 'next/link'
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'
+import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'
-function ScenarioListComponent({
- scenarios,
- portfolioId,
- currentProjectId,
- currentScenarioId,
- onNewScenario,
- onChooseScenario,
- onDeleteScenario,
-}) {
+function ScenarioListComponent({ scenarios, portfolioId, onNewScenario, onDeleteScenario }) {
return (
<>
{scenarios.map((scenario, idx) => (
<Row key={scenario._id} className="mb-1">
- <Col
- xs="7"
- className={classNames('pl-5 align-self-center', {
- 'font-weight-bold': scenario._id === currentScenarioId,
- })}
- >
+ <Col xs="7" className="pl-5 align-self-center">
{scenario.name}
</Col>
<Col xs="5" className="text-right">
- <Link
- passHref
- href={`/projects/${currentProjectId}/portfolios/${scenario.portfolioId}/scenarios/${scenario._id}`}
- >
- <Button
- color="primary"
- outline
- disabled
- className="mr-1"
- onClick={() => onChooseScenario(scenario.portfolioId, scenario._id)}
- >
- <FontAwesomeIcon icon={faPlay} />
- </Button>
- </Link>
<Button
color="danger"
outline
@@ -67,10 +38,7 @@ function ScenarioListComponent({
ScenarioListComponent.propTypes = {
scenarios: PropTypes.arrayOf(Scenario),
portfolioId: PropTypes.string,
- currentProjectId: PropTypes.string.isRequired,
- currentScenarioId: PropTypes.string,
onNewScenario: PropTypes.func.isRequired,
- onChooseScenario: PropTypes.func.isRequired,
onDeleteScenario: PropTypes.func.isRequired,
}
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
index 03b92459..46c639bd 100644
--- 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
@@ -60,7 +60,7 @@ function UnitComponent({ index, unitType, unit, onDelete }) {
UnitComponent.propTypes = {
index: PropTypes.number,
unitType: PropTypes.string,
- unit: PropTypes.oneOf([ProcessingUnit, StorageUnit]),
+ unit: PropTypes.oneOfType([ProcessingUnit, StorageUnit]),
onDelete: PropTypes.func,
}
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 b7da74a1..54c1a6cc 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,12 +1,19 @@
import PropTypes from 'prop-types'
import React from 'react'
-import UnitContainer from '../../../../../containers/app/sidebars/topology/machine/UnitContainer'
+import { ProcessingUnit, StorageUnit } from '../../../../../shapes'
+import UnitComponent from './UnitComponent'
-const UnitListComponent = ({ unitType, unitIds }) => (
+const UnitListComponent = ({ unitType, units, onDelete }) => (
<ul className="list-group mt-1">
- {unitIds.length !== 0 ? (
- unitIds.map((unitId, index) => (
- <UnitContainer unitType={unitType} unitId={unitId} index={index} key={index} />
+ {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">
@@ -19,8 +26,9 @@ const UnitListComponent = ({ unitType, unitIds }) => (
)
UnitListComponent.propTypes = {
- unitType: PropTypes.string,
- unitIds: PropTypes.array,
+ unitType: PropTypes.string.isRequired,
+ units: PropTypes.arrayOf(PropTypes.oneOfType([ProcessingUnit, StorageUnit])).isRequired,
+ onDelete: PropTypes.func,
}
export default UnitListComponent
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 f91202ba..b71918da 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
@@ -23,7 +23,7 @@ UnitIcon.propTypes = {
const MachineComponent = ({ position, machine, onClick }) => {
const hasNoUnits =
- machine.cpuIds.length + machine.gpuIds.length + machine.memoryIds.length + machine.storageIds.length === 0
+ machine.cpus.length + machine.gpus.length + machine.memories.length + machine.storages.length === 0
return (
<ListGroupItem
@@ -36,10 +36,10 @@ const MachineComponent = ({ position, machine, onClick }) => {
{position}
</Badge>
<div className="d-inline-flex">
- {machine.cpuIds.length > 0 ? <UnitIcon id="cpu" type="CPU" /> : undefined}
- {machine.gpuIds.length > 0 ? <UnitIcon id="gpu" type="GPU" /> : undefined}
- {machine.memoryIds.length > 0 ? <UnitIcon id="memory" type="memory" /> : undefined}
- {machine.storageIds.length > 0 ? <UnitIcon id="storage" type="storage" /> : undefined}
+ {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>
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 d0958c28..e024a417 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,17 +1,18 @@
import PropTypes from 'prop-types'
import React from 'react'
-import EmptySlotContainer from '../../../../../containers/app/sidebars/topology/rack/EmptySlotContainer'
-import MachineContainer from '../../../../../containers/app/sidebars/topology/rack/MachineContainer'
import { machineList } from './MachineListComponent.module.scss'
+import MachineComponent from './MachineComponent'
+import { Machine } from '../../../../../shapes'
+import EmptySlotComponent from './EmptySlotComponent'
-const MachineListComponent = ({ machineIds }) => {
+const MachineListComponent = ({ machines = [], onSelect, onAdd }) => {
return (
<ul className={`list-group ${machineList}`}>
- {machineIds.map((machineId, index) => {
- if (machineId === null) {
- return <EmptySlotContainer key={index} position={index + 1} />
+ {machines.map((machine, index) => {
+ if (machine === null) {
+ return <EmptySlotComponent key={index} onAdd={() => onAdd(index + 1)} />
} else {
- return <MachineContainer key={index} position={index + 1} machineId={machineId} />
+ return <MachineComponent key={index} onClick={() => onSelect(index + 1)} machine={machine} />
}
})}
</ul>
@@ -19,7 +20,9 @@ const MachineListComponent = ({ machineIds }) => {
}
MachineListComponent.propTypes = {
- machineIds: PropTypes.array,
+ machines: PropTypes.arrayOf(Machine),
+ onSelect: PropTypes.func.isRequired,
+ onAdd: PropTypes.func.isRequired,
}
export default MachineListComponent
diff --git a/opendc-web/opendc-web-ui/src/containers/app/App.js b/opendc-web/opendc-web-ui/src/containers/app/App.js
deleted file mode 100644
index ec9714ce..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/App.js
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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, { useEffect } from 'react'
-import Head from 'next/head'
-import { HotKeys } from 'react-hotkeys'
-import { useDispatch, useSelector } from 'react-redux'
-import { openPortfolioSucceeded } from '../../redux/actions/portfolios'
-import { openProjectSucceeded } from '../../redux/actions/projects'
-import ToolPanelComponent from '../../components/app/map/controls/ToolPanelComponent'
-import LoadingScreen from '../../components/app/map/LoadingScreen'
-import ScaleIndicatorContainer from '../../containers/app/map/controls/ScaleIndicatorContainer'
-import MapStage from '../../containers/app/map/MapStage'
-import TopologySidebarContainer from '../../containers/app/sidebars/topology/TopologySidebarContainer'
-import AppNavbarContainer from '../../containers/navigation/AppNavbarContainer'
-import ProjectSidebarContainer from '../../containers/app/sidebars/project/ProjectSidebarContainer'
-import { openScenarioSucceeded } from '../../redux/actions/scenarios'
-import PortfolioResultsContainer from '../../containers/app/results/PortfolioResultsContainer'
-import { KeymapConfiguration } from '../../hotkeys'
-import { useRequireAuth } from '../../auth'
-import { useActiveProject } from '../../data/project'
-
-const App = ({ projectId, portfolioId, scenarioId }) => {
- useRequireAuth()
-
- const projectName = useActiveProject()?.name
- const topologyIsLoading = useSelector((state) => state.currentTopologyId === '-1')
-
- const dispatch = useDispatch()
- useEffect(() => {
- if (scenarioId) {
- dispatch(openScenarioSucceeded(projectId, portfolioId, scenarioId))
- } else if (portfolioId) {
- dispatch(openPortfolioSucceeded(projectId, portfolioId))
- } else {
- dispatch(openProjectSucceeded(projectId))
- }
- }, [projectId, portfolioId, scenarioId, dispatch])
-
- const constructionElements = topologyIsLoading ? (
- <div className="full-height d-flex align-items-center justify-content-center">
- <LoadingScreen />
- </div>
- ) : (
- <div className="full-height">
- <MapStage />
- <ScaleIndicatorContainer />
- <ToolPanelComponent />
- <ProjectSidebarContainer />
- <TopologySidebarContainer />
- </div>
- )
-
- const portfolioElements = (
- <div className="full-height app-page-container">
- <ProjectSidebarContainer />
- <div className="container-fluid full-height">
- <PortfolioResultsContainer />
- </div>
- </div>
- )
-
- const scenarioElements = (
- <div className="full-height app-page-container">
- <ProjectSidebarContainer />
- <div className="container-fluid full-height">
- <h2>Scenario loading</h2>
- </div>
- </div>
- )
-
- const title = projectName ? projectName + ' - OpenDC' : 'Simulation - OpenDC'
-
- return (
- <HotKeys keyMap={KeymapConfiguration} allowChanges={true} className="page-container full-height">
- <Head>
- <title>{title}</title>
- </Head>
- <AppNavbarContainer fullWidth={true} />
- {scenarioId ? scenarioElements : portfolioId ? portfolioElements : constructionElements}
- </HotKeys>
- )
-}
-
-App.propTypes = {
- projectId: PropTypes.string.isRequired,
- portfolioId: PropTypes.string,
- scenarioId: PropTypes.string,
-}
-
-export default App
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js
index 00d3152f..d22317a5 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js
@@ -5,17 +5,17 @@ import RackFillBar from '../../../components/app/map/elements/RackFillBar'
const RackSpaceFillContainer = (props) => {
const state = useSelector((state) => {
let energyConsumptionTotal = 0
- const rack = state.objects.rack[state.objects.tile[props.tileId].rackId]
- const machineIds = rack.machineIds
+ const rack = state.objects.rack[state.objects.tile[props.tileId].rack]
+ const machineIds = rack.machines
machineIds.forEach((machineId) => {
if (machineId !== null) {
const machine = state.objects.machine[machineId]
- machine.cpuIds.forEach((id) => (energyConsumptionTotal += state.objects.cpu[id].energyConsumptionW))
- machine.gpuIds.forEach((id) => (energyConsumptionTotal += state.objects.gpu[id].energyConsumptionW))
- machine.memoryIds.forEach(
+ machine.cpus.forEach((id) => (energyConsumptionTotal += state.objects.cpu[id].energyConsumptionW))
+ machine.gpus.forEach((id) => (energyConsumptionTotal += state.objects.gpu[id].energyConsumptionW))
+ machine.memories.forEach(
(id) => (energyConsumptionTotal += state.objects.memory[id].energyConsumptionW)
)
- machine.storageIds.forEach(
+ machine.storages.forEach(
(id) => (energyConsumptionTotal += state.objects.storage[id].energyConsumptionW)
)
}
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
index dc5119fd..8d6f61e0 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js
@@ -4,7 +4,7 @@ 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].rackId].machineIds
+ const machineIds = state.objects.rack[state.objects.tile[props.tileId].rack].machines
return {
type: 'space',
fillFraction: machineIds.filter((id) => id !== null).length / machineIds.length,
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
index 52d48317..0a9e1503 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js
@@ -1,3 +1,4 @@
+import PropTypes from 'prop-types'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { goFromBuildingToRoom } from '../../../redux/actions/interaction-level'
@@ -15,4 +16,8 @@ const RoomContainer = (props) => {
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
index f97e89a1..50a2abfd 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js
@@ -9,7 +9,7 @@ const TileContainer = (props) => {
const dispatch = useDispatch()
const onClick = (tile) => {
- if (tile.rackId) {
+ if (tile.rack) {
dispatch(goFromRoomToRack(tile._id))
}
}
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
index 2a469860..67f36396 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js
@@ -4,7 +4,7 @@ import WallGroup from '../../../components/app/map/groups/WallGroup'
const WallContainer = (props) => {
const tiles = useSelector((state) =>
- state.objects.room[props.roomId].tileIds.map((tileId) => state.objects.tile[tileId])
+ state.objects.room[props.roomId].tiles.map((tileId) => state.objects.tile[tileId])
)
return <WallGroup {...props} tiles={tiles} />
}
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js b/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js
index 8e934a01..e9a64545 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js
@@ -16,10 +16,10 @@ const ObjectHoverLayer = (props) => {
}
const currentRoom = state.objects.room[state.interactionLevel.roomId]
- const tiles = currentRoom.tileIds.map((tileId) => state.objects.tile[tileId])
+ const tiles = currentRoom.tiles.map((tileId) => state.objects.tile[tileId])
const tile = findTileWithPosition(tiles, x, y)
- return !(tile === null || tile.rackId)
+ return !(tile === null || tile.rack)
},
}
})
diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js
index 1bfadb6d..4070c766 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js
@@ -23,14 +23,14 @@ const RoomHoverLayer = (props) => {
.map((id) => Object.assign({}, state.objects.room[id]))
.filter(
(room) =>
- state.objects.topology[state.currentTopologyId].roomIds.indexOf(room._id) !== -1 &&
+ state.objects.topology[state.currentTopologyId].rooms.indexOf(room._id) !== -1 &&
room._id !== state.construction.currentRoomInConstruction
)
;[...oldRooms, newRoom].forEach((room) => {
- room.tiles = room.tileIds.map((tileId) => state.objects.tile[tileId])
+ room.tiles = room.tiles.map((tileId) => state.objects.tile[tileId])
})
- if (newRoom.tileIds.length === 0) {
+ if (newRoom.tiles.length === 0) {
return findPositionInRooms(oldRooms, x, y) === -1
}
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
index e60abe18..a75f15ae 100644
--- a/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js
@@ -1,30 +1,13 @@
import React from 'react'
-import { useSelector } from 'react-redux'
import PortfolioResultsComponent from '../../../components/app/results/PortfolioResultsComponent'
+import { useRouter } from 'next/router'
+import { usePortfolio, usePortfolioScenarios } from '../../../data/project'
const PortfolioResultsContainer = (props) => {
- const { scenarios, portfolio } = useSelector((state) => {
- if (
- state.currentPortfolioId === '-1' ||
- !state.objects.portfolio[state.currentPortfolioId] ||
- state.objects.portfolio[state.currentPortfolioId].scenarioIds
- .map((scenarioId) => state.objects.scenario[scenarioId])
- .some((s) => s === undefined)
- ) {
- return {
- portfolio: undefined,
- scenarios: [],
- }
- }
-
- return {
- portfolio: state.objects.portfolio[state.currentPortfolioId],
- scenarios: state.objects.portfolio[state.currentPortfolioId].scenarioIds.map(
- (scenarioId) => state.objects.scenario[scenarioId]
- ),
- }
- })
-
+ const router = useRouter()
+ const { portfolio: currentPortfolioId } = router.query
+ const { data: portfolio } = usePortfolio(currentPortfolioId)
+ const scenarios = usePortfolioScenarios(currentPortfolioId).data ?? []
return <PortfolioResultsComponent {...props} scenarios={scenarios} portfolio={portfolio} />
}
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
index a36997ff..60ac666c 100644
--- 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
@@ -1,43 +1,34 @@
import React, { useState } from 'react'
-import { useDispatch } from 'react-redux'
import { useRouter } from 'next/router'
import PortfolioListComponent from '../../../../components/app/sidebars/project/PortfolioListComponent'
-import { addPortfolio, deletePortfolio, setCurrentPortfolio } from '../../../../redux/actions/portfolios'
-import { getState } from '../../../../util/state-utils'
-import { setCurrentTopology } from '../../../../redux/actions/topology/building'
import NewPortfolioModalComponent from '../../../../components/modals/custom-components/NewPortfolioModalComponent'
-import { useActivePortfolio, useActiveProject, usePortfolios } from '../../../../data/project'
+import { useProjectPortfolios } from '../../../../data/project'
+import { useMutation } from 'react-query'
const PortfolioListContainer = () => {
- const currentProjectId = useActiveProject()?._id
- const currentPortfolioId = useActivePortfolio()?._id
- const portfolios = usePortfolios(currentProjectId)
+ 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 dispatch = useDispatch()
const [isVisible, setVisible] = useState(false)
- const router = useRouter()
const actions = {
onNewPortfolio: () => setVisible(true),
- onChoosePortfolio: (portfolioId) => {
- dispatch(setCurrentPortfolio(portfolioId))
+ onChoosePortfolio: async (portfolioId) => {
+ await router.push(`/projects/${currentProjectId}/portfolios/${portfolioId}`)
},
onDeletePortfolio: async (id) => {
if (id) {
- const state = await getState(dispatch)
- dispatch(deletePortfolio(id))
- dispatch(setCurrentTopology(state.objects.project[state.currentProjectId].topologyIds[0]))
- router.push(`/projects/${state.currentProjectId}`)
+ await deletePortfolio(id)
+ await router.push(`/projects/${currentProjectId}`)
}
},
}
const callback = (name, targets) => {
if (name) {
- dispatch(
- addPortfolio({
- name,
- targets,
- })
- )
+ addPortfolio({ projectId: currentProjectId, name, targets })
}
setVisible(false)
}
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
index e1be51dc..3b68df38 100644
--- 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
@@ -1,47 +1,39 @@
+import PropTypes from 'prop-types'
import React, { useState } from 'react'
-import { useDispatch } from 'react-redux'
import ScenarioListComponent from '../../../../components/app/sidebars/project/ScenarioListComponent'
-import { addScenario, deleteScenario, setCurrentScenario } from '../../../../redux/actions/scenarios'
-import { setCurrentPortfolio } from '../../../../redux/actions/portfolios'
import NewScenarioModalComponent from '../../../../components/modals/custom-components/NewScenarioModalComponent'
import { useProjectTopologies } from '../../../../data/topology'
-import { useActiveScenario, useActiveProject, useScenarios } from '../../../../data/project'
+import { usePortfolio, usePortfolioScenarios } from '../../../../data/project'
import { useSchedulers, useTraces } from '../../../../data/experiments'
+import { useMutation } from 'react-query'
const ScenarioListContainer = ({ portfolioId }) => {
- const currentProjectId = useActiveProject()?._id
- const currentScenarioId = useActiveScenario()?._id
- const scenarios = useScenarios(portfolioId)
- const topologies = useProjectTopologies()
- const traces = useTraces()
- const schedulers = useSchedulers()
+ 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 dispatch = useDispatch()
const [isVisible, setVisible] = useState(false)
- const onNewScenario = (currentPortfolioId) => {
- dispatch(setCurrentPortfolio(currentPortfolioId))
- setVisible(true)
- }
- const onChooseScenario = (portfolioId, scenarioId) => {
- dispatch(setCurrentScenario(portfolioId, scenarioId))
- }
- const onDeleteScenario = (id) => {
- if (id) {
- dispatch(deleteScenario(id))
- }
- }
+ const onNewScenario = () => setVisible(true)
+ const onDeleteScenario = (id) => id && deleteScenario(id)
const callback = (name, portfolioId, trace, topology, operational) => {
if (name) {
- dispatch(
- addScenario({
- portfolioId,
- name,
- trace,
- topology,
- operational,
- })
- )
+ addScenario({
+ portfolioId,
+ name,
+ trace,
+ topology,
+ operational,
+ })
}
setVisible(false)
@@ -51,11 +43,8 @@ const ScenarioListContainer = ({ portfolioId }) => {
<>
<ScenarioListComponent
portfolioId={portfolioId}
- currentProjectId={currentProjectId}
- currentScenarioId={currentScenarioId}
scenarios={scenarios}
onNewScenario={onNewScenario}
- onChooseScenario={onChooseScenario}
onDeleteScenario={onDeleteScenario}
/>
<NewScenarioModalComponent
@@ -71,4 +60,8 @@ const ScenarioListContainer = ({ portfolioId }) => {
)
}
+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
index 266ca495..a2244a30 100644
--- 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
@@ -3,40 +3,42 @@ 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 { getState } from '../../../../util/state-utils'
-import { addTopology, deleteTopology } from '../../../../redux/actions/topologies'
+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 topologies = useProjectTopologies()
+ 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))
- const state = await getState(dispatch)
- router.push(`/projects/${state.currentProjectId}`)
+ await router.push(`/projects/${currentProjectId}/topologies/${id}`)
}
const onDeleteTopology = async (id) => {
if (id) {
- const state = await getState(dispatch)
- dispatch(deleteTopology(id))
- dispatch(setCurrentTopology(state.objects.project[state.currentProjectId].topologyIds[0]))
- router.push(`/projects/${state.currentProjectId}`)
+ deleteTopology(id)
+ await router.push(`/projects/${currentProjectId}`)
}
}
const onCreateTopology = (name) => {
if (name) {
- dispatch(addTopology(name, undefined))
+ dispatch(addTopology(currentProjectId, name, undefined))
}
setVisible(false)
}
const onDuplicateTopology = (name, id) => {
if (name) {
- dispatch(addTopology(name, id))
+ dispatch(addTopology(currentProjectId, name, id))
}
setVisible(false)
}
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
index cb7ec8f9..7553c2fe 100644
--- 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
@@ -5,7 +5,7 @@ import MachineSidebarComponent from '../../../../../components/app/sidebars/topo
const MachineSidebarContainer = (props) => {
const machineId = useSelector(
(state) =>
- state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].machineIds[
+ state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].machines[
state.interactionLevel.position - 1
]
)
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js
deleted file mode 100644
index acb16a21..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import { deleteUnit } from '../../../../../redux/actions/topology/machine'
-import UnitComponent from '../../../../../components/app/sidebars/topology/machine/UnitComponent'
-
-const UnitContainer = ({ unitId, unitType }) => {
- const dispatch = useDispatch()
- const unit = useSelector((state) => state.objects[unitType][unitId])
- const onDelete = () => dispatch(deleteUnit(unitType, unitId))
-
- return <UnitComponent index={unitId} unit={unit} unitType={unitType} onDelete={onDelete} />
-}
-
-export default UnitContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js
index c5c9444d..cdd7e268 100644
--- 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
@@ -1,17 +1,34 @@
+import PropTypes from 'prop-types'
import React from 'react'
-import { useSelector } from 'react-redux'
+import { useDispatch, useSelector } from 'react-redux'
import UnitListComponent from '../../../../../components/app/sidebars/topology/machine/UnitListComponent'
+import { deleteUnit } from '../../../../../redux/actions/topology/machine'
-const UnitListContainer = (props) => {
- const unitIds = useSelector(
- (state) =>
+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].rackId].machineIds[
+ state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].machines[
state.interactionLevel.position - 1
]
- ][props.unitType + 'Ids']
- )
- return <UnitListComponent {...props} unitIds={unitIds} />
+ ]
+ 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/rack/EmptySlotContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/EmptySlotContainer.js
deleted file mode 100644
index 2134e411..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/EmptySlotContainer.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react'
-import { useDispatch } from 'react-redux'
-import { addMachine } from '../../../../../redux/actions/topology/rack'
-import EmptySlotComponent from '../../../../../components/app/sidebars/topology/rack/EmptySlotComponent'
-
-const EmptySlotContainer = (props) => {
- const dispatch = useDispatch()
- const onAdd = () => dispatch(addMachine(props.position))
- return <EmptySlotComponent {...props} onAdd={onAdd} />
-}
-
-export default EmptySlotContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js
deleted file mode 100644
index 7d8e32c1..00000000
--- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import { goFromRackToMachine } from '../../../../../redux/actions/interaction-level'
-import MachineComponent from '../../../../../components/app/sidebars/topology/rack/MachineComponent'
-
-const MachineContainer = (props) => {
- const machine = useSelector((state) => state.objects.machine[props.machineId])
- const dispatch = useDispatch()
- return (
- <MachineComponent {...props} onClick={() => dispatch(goFromRackToMachine(props.position))} machine={machine} />
- )
-}
-
-export default MachineContainer
diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js
index b45300fc..2118d915 100644
--- 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
@@ -1,12 +1,29 @@
-import React from 'react'
-import { useSelector } from 'react-redux'
+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 machineIds = useSelector(
- (state) => state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].machineIds
+ 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))}
+ />
)
- return <MachineListComponent {...props} machineIds={machineIds} />
}
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
index eaa1e78e..2c39cf9f 100644
--- 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
@@ -7,7 +7,7 @@ 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].rackId].name
+ (state) => state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rack].name
)
const dispatch = useDispatch()
const callback = (name) => {
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
index b8fc3bfb..34777125 100644
--- 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
@@ -3,7 +3,7 @@ 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].rackId)
+ const rackId = useSelector((state) => state.objects.tile[state.interactionLevel.tileId].rack)
return <RackSidebarComponent {...props} rackId={rackId} />
}
diff --git a/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js b/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js
index 6742bc26..ff9f9fe7 100644
--- a/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js
@@ -1,9 +1,10 @@
import React from 'react'
import AppNavbarComponent from '../../components/navigation/AppNavbarComponent'
-import { useActiveProject } from '../../data/project'
+import { useActiveProjectId, useProject } from '../../data/project'
const AppNavbarContainer = (props) => {
- const project = useActiveProject()
+ const projectId = useActiveProjectId()
+ const { data: project } = useProject(projectId)
return <AppNavbarComponent {...props} project={project} />
}
diff --git a/opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js b/opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js
index e03b5c07..ac0edae4 100644
--- a/opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/projects/NewProjectContainer.js
@@ -1,20 +1,19 @@
import React, { useState } from 'react'
-import { useDispatch } from 'react-redux'
-import { addProject } from '../../redux/actions/projects'
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 dispatch = useDispatch()
+ const { mutate: addProject } = useMutation('addProject')
const callback = (text) => {
if (text) {
- dispatch(addProject(text))
+ addProject({ name: text })
}
setVisible(false)
}
diff --git a/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js b/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js
index bdb422dc..62985742 100644
--- a/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js
+++ b/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js
@@ -1,13 +1,12 @@
import React from 'react'
-import { useDispatch } from 'react-redux'
-import { deleteProject } from '../../redux/actions/projects'
import ProjectActionButtons from '../../components/projects/ProjectActionButtons'
+import { useMutation } from 'react-query'
const ProjectActions = (props) => {
- const dispatch = useDispatch()
+ const { mutate: deleteProject } = useMutation('deleteProject')
const actions = {
onViewUsers: (id) => {}, // TODO implement user viewing
- onDelete: (id) => dispatch(deleteProject(id)),
+ onDelete: (id) => deleteProject(id),
}
return <ProjectActionButtons {...props} {...actions} />
}
diff --git a/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js b/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js
index 6632a8b5..b5c5dd68 100644
--- a/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js
+++ b/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js
@@ -23,8 +23,8 @@ const getVisibleProjects = (projects, filter, userId) => {
const ProjectListContainer = ({ filter }) => {
const { user } = useAuth()
- const projects = useProjects()
- return <ProjectList projects={getVisibleProjects(projects, filter, user?.sub)} />
+ const { data: projects } = useProjects()
+ return <ProjectList projects={getVisibleProjects(projects ?? [], filter, user?.sub)} />
}
ProjectListContainer.propTypes = {
diff --git a/opendc-web/opendc-web-ui/src/data/experiments.js b/opendc-web/opendc-web-ui/src/data/experiments.js
index aef512e5..a76ea53f 100644
--- a/opendc-web/opendc-web-ui/src/data/experiments.js
+++ b/opendc-web/opendc-web-ui/src/data/experiments.js
@@ -20,18 +20,28 @@
* SOFTWARE.
*/
-import { useSelector } from 'react-redux'
+import { useQuery } from 'react-query'
+import { fetchTraces } from '../api/traces'
+import { fetchSchedulers } from '../api/schedulers'
+
+/**
+ * Configure the query defaults for the experiment endpoints.
+ */
+export function configureExperimentClient(queryClient, auth) {
+ queryClient.setQueryDefaults('traces', { queryFn: () => fetchTraces(auth) })
+ queryClient.setQueryDefaults('schedulers', { queryFn: () => fetchSchedulers(auth) })
+}
/**
* Return the available traces to experiment with.
*/
export function useTraces() {
- return useSelector((state) => Object.values(state.objects.trace))
+ return useQuery('traces')
}
/**
* Return the available schedulers to experiment with.
*/
export function useSchedulers() {
- return useSelector((state) => Object.values(state.objects.scheduler))
+ return useQuery('schedulers')
}
diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js
index de2bc0d3..9bdcfb93 100644
--- a/opendc-web/opendc-web-ui/src/data/project.js
+++ b/opendc-web/opendc-web-ui/src/data/project.js
@@ -20,66 +20,143 @@
* SOFTWARE.
*/
-import { useSelector } from 'react-redux'
+import { useQueries, 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'
+
+/**
+ * Configure the query defaults for the project endpoints.
+ */
+export function configureProjectClient(queryClient, auth) {
+ queryClient.setQueryDefaults('projects', {
+ queryFn: ({ queryKey }) => (queryKey.length === 1 ? fetchProjects(auth) : fetchProject(auth, queryKey[1])),
+ })
+
+ queryClient.setMutationDefaults('addProject', {
+ mutationFn: (data) => addProject(auth, data),
+ onSuccess: async (result) => {
+ queryClient.setQueryData('projects', (old = []) => [...old, result])
+ },
+ })
+ queryClient.setMutationDefaults('deleteProject', {
+ mutationFn: (id) => deleteProject(auth, id),
+ onSuccess: async (result) => {
+ queryClient.setQueryData('projects', (old = []) => old.filter((project) => project._id !== result._id))
+ queryClient.removeQueries(['projects', result._id])
+ },
+ })
+
+ queryClient.setQueryDefaults('portfolios', {
+ queryFn: ({ queryKey }) => fetchPortfolio(auth, queryKey[1]),
+ })
+ queryClient.setQueryDefaults('project-portfolios', {
+ queryFn: ({ queryKey }) => fetchPortfoliosOfProject(auth, queryKey[1]),
+ })
+ queryClient.setMutationDefaults('addPortfolio', {
+ mutationFn: (data) => addPortfolio(auth, data),
+ onSuccess: async (result) => {
+ queryClient.setQueryData(['projects', result.projectId], (old) => ({
+ ...old,
+ portfolioIds: [...old.portfolioIds, result._id],
+ }))
+ queryClient.setQueryData(['portfolios', result._id], result)
+ },
+ })
+ queryClient.setMutationDefaults('deletePortfolio', {
+ mutationFn: (id) => deletePortfolio(auth, id),
+ onSuccess: async (result) => {
+ queryClient.setQueryData(['projects', result.projectId], (old) => ({
+ ...old,
+ portfolioIds: old.portfolioIds.filter((id) => id !== result._id),
+ }))
+ queryClient.removeQueries(['portfolios', result._id])
+ },
+ })
+
+ queryClient.setQueryDefaults('scenarios', {
+ queryFn: ({ queryKey }) => fetchScenario(auth, queryKey[1]),
+ })
+ queryClient.setQueryDefaults('portfolio-scenarios', {
+ queryFn: ({ queryKey }) => fetchScenariosOfPortfolio(auth, queryKey[1]),
+ })
+ queryClient.setMutationDefaults('addScenario', {
+ mutationFn: (data) => addScenario(auth, data),
+ onSuccess: async (result) => {
+ // Register updated scenario in cache
+ queryClient.setQueryData(['scenarios', result._id], result)
+
+ // Add scenario id to portfolio
+ queryClient.setQueryData(['portfolios', result.portfolioId], (old) => ({
+ ...old,
+ scenarioIds: [...old.scenarioIds, result._id],
+ }))
+ },
+ })
+ queryClient.setMutationDefaults('deleteScenario', {
+ mutationFn: (id) => deleteScenario(auth, id),
+ onSuccess: async (result) => {
+ queryClient.setQueryData(['portfolios', result.portfolioId], (old) => ({
+ ...old,
+ scenarioIds: old.scenarioIds.filter((id) => id !== result._id),
+ }))
+ queryClient.removeQueries(['scenarios', result._id])
+ },
+ })
+}
/**
* Return the available projects.
*/
export function useProjects() {
- return useSelector((state) => state.projects)
+ return useQuery('projects')
}
/**
- * Return the current active project.
+ * Return the project with the specified identifier.
*/
-export function useActiveProject() {
- return useSelector((state) =>
- state.currentProjectId !== '-1' ? state.objects.project[state.currentProjectId] : undefined
- )
+export function useProject(projectId) {
+ return useQuery(['projects', projectId], { enabled: !!projectId })
}
/**
- * Return the active portfolio.
+ * Return the portfolio with the specified identifier.
*/
-export function useActivePortfolio() {
- return useSelector((state) => state.objects.portfolio[state.currentPortfolioId])
+export function usePortfolio(portfolioId) {
+ return useQuery(['portfolios', portfolioId], { enabled: !!portfolioId })
}
/**
- * Return the active scenario.
+ * Return the portfolios of the specified project.
*/
-export function useActiveScenario() {
- return useSelector((state) => state.objects.scenario[state.currentScenarioId])
+export function useProjectPortfolios(projectId) {
+ return useQuery(['project-portfolios', projectId], { enabled: !!projectId })
}
/**
- * Return the portfolios for the specified project id.
+ * Return the scenarios with the specified identifiers.
*/
-export function usePortfolios(projectId) {
- return useSelector((state) => {
- let portfolios = state.objects.project[projectId]
- ? state.objects.project[projectId].portfolioIds.map((t) => state.objects.portfolio[t])
- : []
- if (portfolios.filter((t) => !t).length > 0) {
- portfolios = []
- }
-
- return portfolios
- })
+export function useScenarios(scenarioIds) {
+ return useQueries(
+ scenarioIds.map((scenarioId) => ({
+ queryKey: ['scenarios', scenarioId],
+ }))
+ )
}
/**
- * Return the scenarios for the specified portfolio id.
+ * Return the scenarios of the specified portfolio.
*/
-export function useScenarios(portfolioId) {
- return useSelector((state) => {
- let scenarios = state.objects.portfolio[portfolioId]
- ? state.objects.portfolio[portfolioId].scenarioIds.map((t) => state.objects.scenario[t])
- : []
- if (scenarios.filter((t) => !t).length > 0) {
- scenarios = []
- }
+export function usePortfolioScenarios(portfolioId) {
+ return useQuery(['portfolio-scenarios', portfolioId], { enabled: !!portfolioId })
+}
- return scenarios
- })
+/**
+ * Return the current active project identifier.
+ */
+export function useActiveProjectId() {
+ const router = useRouter()
+ const { project } = router.query
+ return project
}
diff --git a/opendc-web/opendc-web-ui/src/data/topology.js b/opendc-web/opendc-web-ui/src/data/topology.js
index d3ffb3e1..8db75877 100644
--- a/opendc-web/opendc-web-ui/src/data/topology.js
+++ b/opendc-web/opendc-web-ui/src/data/topology.js
@@ -21,6 +21,43 @@
*/
import { useSelector } from 'react-redux'
+import { useQueries, useQuery } from 'react-query'
+import { addTopology, deleteTopology, fetchTopologiesOfProject, fetchTopology, updateTopology } from '../api/topologies'
+
+/**
+ * Configure the query defaults for the topology endpoints.
+ */
+export function configureTopologyClient(queryClient, auth) {
+ queryClient.setQueryDefaults('topologies', { queryFn: ({ queryKey }) => fetchTopology(auth, queryKey[1]) })
+ queryClient.setQueryDefaults('project-topologies', {
+ queryFn: ({ queryKey }) => fetchTopologiesOfProject(auth, queryKey[1]),
+ })
+
+ queryClient.setMutationDefaults('addTopology', {
+ mutationFn: (data) => addTopology(auth, data),
+ onSuccess: async (result) => {
+ queryClient.setQueryData(['projects', result.projectId], (old) => ({
+ ...old,
+ topologyIds: [...old.topologyIds, result._id],
+ }))
+ queryClient.setQueryData(['topologies', result._id], result)
+ },
+ })
+ queryClient.setMutationDefaults('updateTopology', {
+ mutationFn: (data) => updateTopology(auth, data),
+ onSuccess: async (result) => queryClient.setQueryData(['topologies', result._id], result),
+ })
+ queryClient.setMutationDefaults('deleteTopology', {
+ mutationFn: (id) => deleteTopology(auth, id),
+ onSuccess: async (result) => {
+ queryClient.setQueryData(['projects', result.projectId], (old) => ({
+ ...old,
+ topologyIds: old.topologyIds.filter((id) => id !== result._id),
+ }))
+ queryClient.removeQueries(['topologies', result._id])
+ },
+ })
+}
/**
* Return the current active topology.
@@ -30,20 +67,8 @@ export function useActiveTopology() {
}
/**
- * Return the topologies for the active project.
+ * Return the topologies of the specified project.
*/
-export function useProjectTopologies() {
- return useSelector(({ currentProjectId, objects }) => {
- if (currentProjectId === '-1' || !objects.project[currentProjectId]) {
- return []
- }
-
- const topologies = objects.project[currentProjectId].topologyIds.map((t) => objects.topology[t])
-
- if (topologies.filter((t) => !t).length > 0) {
- return []
- }
-
- return topologies
- })
+export function useProjectTopologies(projectId) {
+ return useQuery(['project-topologies', projectId], { enabled: !!projectId })
}
diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js
index c1adbd6e..6a7200d5 100644
--- a/opendc-web/opendc-web-ui/src/pages/_app.js
+++ b/opendc-web/opendc-web-ui/src/pages/_app.js
@@ -28,15 +28,30 @@ import '../index.scss'
import { AuthProvider, useAuth } from '../auth'
import * as Sentry from '@sentry/react'
import { Integrations } from '@sentry/tracing'
+import { QueryClient, QueryClientProvider } from 'react-query'
+import { useMemo } from 'react'
+import { configureProjectClient } from '../data/project'
+import { configureExperimentClient } from '../data/experiments'
+import { configureTopologyClient } from '../data/topology'
// This setup is necessary to forward the Auth0 context to the Redux context
const Inner = ({ Component, pageProps }) => {
const auth = useAuth()
- const store = useStore(pageProps.initialReduxState, { auth })
+
+ const queryClient = useMemo(() => {
+ const client = new QueryClient()
+ configureProjectClient(client, auth)
+ configureExperimentClient(client, auth)
+ configureTopologyClient(client, auth)
+ return client
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
+ const store = useStore(pageProps.initialReduxState, { auth, queryClient })
return (
- <Provider store={store}>
- <Component {...pageProps} />
- </Provider>
+ <QueryClientProvider client={queryClient}>
+ <Provider store={store}>
+ <Component {...pageProps} />
+ </Provider>
+ </QueryClientProvider>
)
}
diff --git a/opendc-web/opendc-web-ui/src/pages/_document.js b/opendc-web/opendc-web-ui/src/pages/_document.js
index 8e4680c0..51d8d3e0 100644
--- a/opendc-web/opendc-web-ui/src/pages/_document.js
+++ b/opendc-web/opendc-web-ui/src/pages/_document.js
@@ -25,7 +25,7 @@ import Document, { Html, Head, Main, NextScript } from 'next/document'
class OpenDCDocument extends Document {
render() {
return (
- <Html>
+ <Html lang="en">
<Head>
<meta charSet="utf-8" />
<meta name="theme-color" content="#00A6D6" />
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 72316bc9..cce887aa 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,18 +20,6 @@
* SOFTWARE.
*/
-import { useRouter } from 'next/router'
-import App from '../../../containers/app/App'
+import Topology from './topologies/[topology]'
-function Project() {
- const router = useRouter()
- const { project } = router.query
-
- if (project) {
- return <App projectId={project} />
- }
-
- return <div />
-}
-
-export default Project
+export default Topology
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 76a8d23b..d3d61271 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
@@ -21,17 +21,40 @@
*/
import { useRouter } from 'next/router'
-import App from '../../../../containers/app/App'
+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'
-function Project() {
+/**
+ * Page that displays the results in a portfolio.
+ */
+function Portfolio() {
const router = useRouter()
- const { project, portfolio } = router.query
+ const { project: projectId, portfolio: portfolioId } = router.query
+
+ const project = useProject(projectId)
+ const title = project?.name ? project?.name + ' - OpenDC' : 'Simulation - OpenDC'
- if (project && portfolio) {
- return <App projectId={project} portfolioId={portfolio} />
- }
+ const dispatch = useDispatch()
- return <div />
+ return (
+ <div className="page-container full-height">
+ <Head>
+ <title>{title}</title>
+ </Head>
+ <AppNavbarContainer fullWidth={true} />
+ <div className="full-height app-page-container">
+ <ProjectSidebarContainer />
+ <div className="container-fluid full-height">
+ <PortfolioResultsContainer />
+ </div>
+ </div>
+ </div>
+ )
}
-export default Project
+export default Portfolio
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
new file mode 100644
index 00000000..a9dfdb19
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/pages/projects/[project]/topologies/[topology].js
@@ -0,0 +1,81 @@
+/*
+ * 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 { useRouter } from 'next/router'
+import { useProject } from '../../../../data/project'
+import { useDispatch, useSelector } from 'react-redux'
+import React, { useEffect } 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 { openProjectSucceeded } from '../../../../redux/actions/projects'
+
+/**
+ * Page that displays a datacenter topology.
+ */
+function Topology() {
+ const router = useRouter()
+ 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(() => {
+ if (projectId) {
+ dispatch(openProjectSucceeded(projectId))
+ }
+ }, [projectId, topologyId, dispatch])
+
+ const topologyIsLoading = useSelector((state) => state.currentTopologyId === '-1')
+
+ return (
+ <HotKeys keyMap={KeymapConfiguration} allowChanges={true} className="page-container full-height">
+ <Head>
+ <title>{title}</title>
+ </Head>
+ <AppNavbarContainer fullWidth={true} />
+ {topologyIsLoading ? (
+ <div className="full-height d-flex align-items-center justify-content-center">
+ <LoadingScreen />
+ </div>
+ ) : (
+ <div className="full-height">
+ <MapStage />
+ <ScaleIndicatorContainer />
+ <ToolPanelComponent />
+ <ProjectSidebarContainer />
+ <TopologySidebarContainer />
+ </div>
+ )}
+ </HotKeys>
+ )
+}
+
+export default Topology
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 958ca622..2d8e6de7 100644
--- a/opendc-web/opendc-web-ui/src/pages/projects/index.js
+++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js
@@ -1,22 +1,17 @@
-import React, { useEffect, useState } from 'react'
+import React, { useState } from 'react'
import Head from 'next/head'
-import { useDispatch } from 'react-redux'
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 { fetchProjects } from '../../redux/actions/projects'
function Projects() {
useRequireAuth()
- const dispatch = useDispatch()
const [filter, setFilter] = useState('SHOW_ALL')
- useEffect(() => dispatch(fetchProjects()), [dispatch])
-
return (
<>
<Head>
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js b/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js
deleted file mode 100644
index d37886d8..00000000
--- a/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js
+++ /dev/null
@@ -1,41 +0,0 @@
-export const ADD_PORTFOLIO = 'ADD_PORTFOLIO'
-export const UPDATE_PORTFOLIO = 'UPDATE_PORTFOLIO'
-export const DELETE_PORTFOLIO = 'DELETE_PORTFOLIO'
-export const OPEN_PORTFOLIO_SUCCEEDED = 'OPEN_PORTFOLIO_SUCCEEDED'
-export const SET_CURRENT_PORTFOLIO = 'SET_CURRENT_PORTFOLIO'
-
-export function addPortfolio(portfolio) {
- return {
- type: ADD_PORTFOLIO,
- portfolio,
- }
-}
-
-export function updatePortfolio(portfolio) {
- return {
- type: UPDATE_PORTFOLIO,
- portfolio,
- }
-}
-
-export function deletePortfolio(id) {
- return {
- type: DELETE_PORTFOLIO,
- id,
- }
-}
-
-export function openPortfolioSucceeded(projectId, portfolioId) {
- return {
- type: OPEN_PORTFOLIO_SUCCEEDED,
- projectId,
- portfolioId,
- }
-}
-
-export function setCurrentPortfolio(portfolioId) {
- return {
- type: SET_CURRENT_PORTFOLIO,
- portfolioId,
- }
-}
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/projects.js b/opendc-web/opendc-web-ui/src/redux/actions/projects.js
index a6324c43..4fe6f6a8 100644
--- a/opendc-web/opendc-web-ui/src/redux/actions/projects.js
+++ b/opendc-web/opendc-web-ui/src/redux/actions/projects.js
@@ -1,52 +1,5 @@
-export const FETCH_PROJECTS = 'FETCH_PROJECTS'
-export const FETCH_PROJECTS_SUCCEEDED = 'FETCH_PROJECTS_SUCCEEDED'
-export const ADD_PROJECT = 'ADD_PROJECT'
-export const ADD_PROJECT_SUCCEEDED = 'ADD_PROJECT_SUCCEEDED'
-export const DELETE_PROJECT = 'DELETE_PROJECT'
-export const DELETE_PROJECT_SUCCEEDED = 'DELETE_PROJECT_SUCCEEDED'
export const OPEN_PROJECT_SUCCEEDED = 'OPEN_PROJECT_SUCCEEDED'
-export function fetchProjects() {
- return {
- type: FETCH_PROJECTS,
- }
-}
-
-export function fetchProjectsSucceeded(projects) {
- return {
- type: FETCH_PROJECTS_SUCCEEDED,
- projects,
- }
-}
-
-export function addProject(name) {
- return {
- type: ADD_PROJECT,
- name,
- }
-}
-
-export function addProjectSucceeded(project) {
- return {
- type: ADD_PROJECT_SUCCEEDED,
- project,
- }
-}
-
-export function deleteProject(id) {
- return {
- type: DELETE_PROJECT,
- id,
- }
-}
-
-export function deleteProjectSucceeded(id) {
- return {
- type: DELETE_PROJECT_SUCCEEDED,
- id,
- }
-}
-
export function openProjectSucceeded(id) {
return {
type: OPEN_PROJECT_SUCCEEDED,
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js b/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js
deleted file mode 100644
index c8a90762..00000000
--- a/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js
+++ /dev/null
@@ -1,43 +0,0 @@
-export const ADD_SCENARIO = 'ADD_SCENARIO'
-export const UPDATE_SCENARIO = 'UPDATE_SCENARIO'
-export const DELETE_SCENARIO = 'DELETE_SCENARIO'
-export const OPEN_SCENARIO_SUCCEEDED = 'OPEN_SCENARIO_SUCCEEDED'
-export const SET_CURRENT_SCENARIO = 'SET_CURRENT_SCENARIO'
-
-export function addScenario(scenario) {
- return {
- type: ADD_SCENARIO,
- scenario,
- }
-}
-
-export function updateScenario(scenario) {
- return {
- type: UPDATE_SCENARIO,
- scenario,
- }
-}
-
-export function deleteScenario(id) {
- return {
- type: DELETE_SCENARIO,
- id,
- }
-}
-
-export function openScenarioSucceeded(projectId, portfolioId, scenarioId) {
- return {
- type: OPEN_SCENARIO_SUCCEEDED,
- projectId,
- portfolioId,
- scenarioId,
- }
-}
-
-export function setCurrentScenario(portfolioId, scenarioId) {
- return {
- type: SET_CURRENT_SCENARIO,
- portfolioId,
- scenarioId,
- }
-}
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topologies.js b/opendc-web/opendc-web-ui/src/redux/actions/topologies.js
index dcce3b7d..529e8663 100644
--- a/opendc-web/opendc-web-ui/src/redux/actions/topologies.js
+++ b/opendc-web/opendc-web-ui/src/redux/actions/topologies.js
@@ -1,17 +1,18 @@
export const ADD_TOPOLOGY = 'ADD_TOPOLOGY'
-export const DELETE_TOPOLOGY = 'DELETE_TOPOLOGY'
+export const STORE_TOPOLOGY = 'STORE_TOPOLOGY'
-export function addTopology(name, duplicateId) {
+export function addTopology(projectId, name, duplicateId) {
return {
type: ADD_TOPOLOGY,
+ projectId,
name,
duplicateId,
}
}
-export function deleteTopology(id) {
+export function storeTopology(entities) {
return {
- type: DELETE_TOPOLOGY,
- id,
+ type: STORE_TOPOLOGY,
+ entities,
}
}
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js
index 72deda6f..f1a7d569 100644
--- a/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js
+++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js
@@ -32,7 +32,7 @@ export function startNewRoomConstructionSucceeded(roomId) {
export function finishNewRoomConstruction() {
return (dispatch, getState) => {
const { objects, construction } = getState()
- if (objects.room[construction.currentRoomInConstruction].tileIds.length === 0) {
+ if (objects.room[construction.currentRoomInConstruction].tiles.length === 0) {
dispatch(cancelNewRoomConstruction())
return
}
@@ -75,13 +75,10 @@ export function toggleTileAtLocation(positionX, positionY) {
return (dispatch, getState) => {
const { objects, construction } = getState()
- const tileIds = objects.room[construction.currentRoomInConstruction].tileIds
- for (let index in tileIds) {
- if (
- objects.tile[tileIds[index]].positionX === positionX &&
- objects.tile[tileIds[index]].positionY === positionY
- ) {
- dispatch(deleteTile(tileIds[index]))
+ const tileIds = objects.room[construction.currentRoomInConstruction].tiles
+ for (const tileId of tileIds) {
+ if (objects.tile[tileId].positionX === positionX && objects.tile[tileId].positionY === positionY) {
+ dispatch(deleteTile(tileId))
return
}
}
diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js
index 61eea7fe..80ef7c5e 100644
--- a/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js
+++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js
@@ -29,7 +29,7 @@ export function addRackToTile(positionX, positionY) {
return (dispatch, getState) => {
const { objects, interactionLevel } = getState()
const currentRoom = objects.room[interactionLevel.roomId]
- const tiles = currentRoom.tileIds.map((tileId) => objects.tile[tileId])
+ const tiles = currentRoom.tiles.map((tileId) => objects.tile[tileId])
const tile = findTileWithPosition(tiles, positionX, positionY)
if (tile !== null) {
diff --git a/opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js b/opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js
index 6b22eb80..c2fc5004 100644
--- a/opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js
+++ b/opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js
@@ -22,10 +22,10 @@ export const viewportAdjustmentMiddleware = (store) => (next) => (action) => {
mapDimensions = { width: action.width, height: action.height }
}
- if (topologyId !== '-1') {
- const roomIds = state.objects.topology[topologyId].roomIds
+ if (topologyId && topologyId !== '-1') {
+ const roomIds = state.objects.topology[topologyId].rooms
const rooms = roomIds.map((id) => Object.assign({}, state.objects.room[id]))
- rooms.forEach((room) => (room.tiles = room.tileIds.map((tileId) => state.objects.tile[tileId])))
+ rooms.forEach((room) => (room.tiles = room.tiles.map((tileId) => state.objects.tile[tileId])))
let hasNoTiles = true
for (let i in rooms) {
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js b/opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js
index 257dddd2..5bac7fea 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js
@@ -9,8 +9,6 @@ import {
START_ROOM_EDIT,
} from '../actions/topology/building'
import { DELETE_ROOM, START_RACK_CONSTRUCTION, STOP_RACK_CONSTRUCTION } from '../actions/topology/room'
-import { OPEN_PORTFOLIO_SUCCEEDED } from '../actions/portfolios'
-import { OPEN_SCENARIO_SUCCEEDED } from '../actions/scenarios'
export function currentRoomInConstruction(state = '-1', action) {
switch (action.type) {
@@ -20,8 +18,6 @@ export function currentRoomInConstruction(state = '-1', action) {
return action.roomId
case CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED:
case FINISH_NEW_ROOM_CONSTRUCTION:
- case OPEN_PORTFOLIO_SUCCEEDED:
- case OPEN_SCENARIO_SUCCEEDED:
case FINISH_ROOM_EDIT:
case SET_CURRENT_TOPOLOGY:
case DELETE_ROOM:
@@ -36,8 +32,6 @@ export function inRackConstructionMode(state = false, action) {
case START_RACK_CONSTRUCTION:
return true
case STOP_RACK_CONSTRUCTION:
- case OPEN_PORTFOLIO_SUCCEEDED:
- case OPEN_SCENARIO_SUCCEEDED:
case SET_CURRENT_TOPOLOGY:
case GO_DOWN_ONE_INTERACTION_LEVEL:
return false
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js b/opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js
index 9b46aa60..c0baf567 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js
@@ -1,7 +1,4 @@
-import { OPEN_PORTFOLIO_SUCCEEDED, SET_CURRENT_PORTFOLIO } from '../actions/portfolios'
-import { OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building'
-import { OPEN_SCENARIO_SUCCEEDED, SET_CURRENT_SCENARIO } from '../actions/scenarios'
export function currentTopologyId(state = '-1', action) {
switch (action.type) {
@@ -11,44 +8,3 @@ export function currentTopologyId(state = '-1', action) {
return state
}
}
-
-export function currentProjectId(state = '-1', action) {
- switch (action.type) {
- case OPEN_PROJECT_SUCCEEDED:
- return action.id
- case OPEN_PORTFOLIO_SUCCEEDED:
- case OPEN_SCENARIO_SUCCEEDED:
- return action.projectId
- default:
- return state
- }
-}
-
-export function currentPortfolioId(state = '-1', action) {
- switch (action.type) {
- case OPEN_PORTFOLIO_SUCCEEDED:
- case SET_CURRENT_PORTFOLIO:
- case SET_CURRENT_SCENARIO:
- return action.portfolioId
- case OPEN_SCENARIO_SUCCEEDED:
- return action.portfolioId
- case OPEN_PROJECT_SUCCEEDED:
- case SET_CURRENT_TOPOLOGY:
- return '-1'
- default:
- return state
- }
-}
-export function currentScenarioId(state = '-1', action) {
- switch (action.type) {
- case OPEN_SCENARIO_SUCCEEDED:
- case SET_CURRENT_SCENARIO:
- return action.scenarioId
- case OPEN_PORTFOLIO_SUCCEEDED:
- case SET_CURRENT_TOPOLOGY:
- case OPEN_PROJECT_SUCCEEDED:
- return '-1'
- default:
- return state
- }
-}
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/index.js b/opendc-web/opendc-web-ui/src/redux/reducers/index.js
index b143d417..1b17a206 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/index.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/index.js
@@ -1,20 +1,15 @@
import { combineReducers } from 'redux'
import { construction } from './construction-mode'
-import { currentPortfolioId, currentProjectId, currentScenarioId, currentTopologyId } from './current-ids'
+import { currentTopologyId } from './current-ids'
import { interactionLevel } from './interaction-level'
import { map } from './map'
import { objects } from './objects'
-import { projects } from './projects'
const rootReducer = combineReducers({
objects,
- projects,
construction,
map,
- currentProjectId,
currentTopologyId,
- currentPortfolioId,
- currentScenarioId,
interactionLevel,
})
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js b/opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js
index eafcb269..9f23949f 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js
@@ -1,19 +1,13 @@
-import { OPEN_PORTFOLIO_SUCCEEDED } from '../actions/portfolios'
import {
GO_DOWN_ONE_INTERACTION_LEVEL,
GO_FROM_BUILDING_TO_ROOM,
GO_FROM_RACK_TO_MACHINE,
GO_FROM_ROOM_TO_RACK,
} from '../actions/interaction-level'
-import { OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building'
-import { OPEN_SCENARIO_SUCCEEDED } from '../actions/scenarios'
export function interactionLevel(state = { mode: 'BUILDING' }, action) {
switch (action.type) {
- case OPEN_PORTFOLIO_SUCCEEDED:
- case OPEN_SCENARIO_SUCCEEDED:
- case OPEN_PROJECT_SUCCEEDED:
case SET_CURRENT_TOPOLOGY:
return {
mode: 'BUILDING',
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/objects.js b/opendc-web/opendc-web-ui/src/redux/reducers/objects.js
index a2483b43..11f6d353 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/objects.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/objects.js
@@ -6,11 +6,9 @@ import {
REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP,
} from '../actions/objects'
import { CPU_UNITS, GPU_UNITS, MEMORY_UNITS, STORAGE_UNITS } from '../../util/unit-specifications'
+import { STORE_TOPOLOGY } from '../actions/topologies'
export const objects = combineReducers({
- project: object('project'),
- user: object('user'),
- authorization: objectWithId('authorization', (object) => [object.userId, object.projectId]),
cpu: object('cpu', CPU_UNITS),
gpu: object('gpu', GPU_UNITS),
memory: object('memory', MEMORY_UNITS),
@@ -20,10 +18,6 @@ export const objects = combineReducers({
tile: object('tile'),
room: object('room'),
topology: object('topology'),
- trace: object('trace'),
- scheduler: object('scheduler'),
- portfolio: object('portfolio'),
- scenario: object('scenario'),
prefab: object('prefab'),
})
@@ -33,18 +27,16 @@ function object(type, defaultState = {}) {
function objectWithId(type, getId, defaultState = {}) {
return (state = defaultState, action) => {
- if (action.objectType !== type) {
+ if (action.type === STORE_TOPOLOGY) {
+ return { ...state, ...action.entities[type] }
+ } else if (action.objectType !== type) {
return state
}
if (action.type === ADD_TO_STORE) {
- return Object.assign({}, state, {
- [getId(action.object)]: action.object,
- })
+ return { ...state, [getId(action.object)]: action.object }
} else if (action.type === ADD_PROP_TO_STORE_OBJECT) {
- return Object.assign({}, state, {
- [action.objectId]: Object.assign({}, state[action.objectId], action.propObject),
- })
+ return { ...state, [action.objectId]: { ...state[action.objectId], ...action.propObject } }
} else if (action.type === ADD_ID_TO_STORE_OBJECT_LIST_PROP) {
return Object.assign({}, state, {
[action.objectId]: Object.assign({}, state[action.objectId], {
diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/projects.js b/opendc-web/opendc-web-ui/src/redux/reducers/projects.js
deleted file mode 100644
index a920e47f..00000000
--- a/opendc-web/opendc-web-ui/src/redux/reducers/projects.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { ADD_PROJECT_SUCCEEDED, DELETE_PROJECT_SUCCEEDED, FETCH_PROJECTS_SUCCEEDED } from '../actions/projects'
-
-export function projects(state = [], action) {
- switch (action.type) {
- case FETCH_PROJECTS_SUCCEEDED:
- return action.projects
- case ADD_PROJECT_SUCCEEDED:
- return [...state, action.project]
- case DELETE_PROJECT_SUCCEEDED:
- return state.filter((project) => project._id !== action.id)
- default:
- return state
- }
-}
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/index.js b/opendc-web/opendc-web-ui/src/redux/sagas/index.js
index a8f44843..318f0afb 100644
--- a/opendc-web/opendc-web-ui/src/redux/sagas/index.js
+++ b/opendc-web/opendc-web-ui/src/redux/sagas/index.js
@@ -1,6 +1,5 @@
import { takeEvery } from 'redux-saga/effects'
-import { ADD_PORTFOLIO, DELETE_PORTFOLIO, OPEN_PORTFOLIO_SUCCEEDED, UPDATE_PORTFOLIO } from '../actions/portfolios'
-import { ADD_PROJECT, DELETE_PROJECT, FETCH_PROJECTS, OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
+import { OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
import {
ADD_TILE,
CANCEL_NEW_ROOM_CONSTRUCTION,
@@ -10,8 +9,7 @@ import {
import { ADD_UNIT, DELETE_MACHINE, DELETE_UNIT } from '../actions/topology/machine'
import { ADD_MACHINE, DELETE_RACK, EDIT_RACK_NAME } from '../actions/topology/rack'
import { ADD_RACK_TO_TILE, DELETE_ROOM, EDIT_ROOM_NAME } from '../actions/topology/room'
-import { onAddPortfolio, onDeletePortfolio, onOpenPortfolioSucceeded, onUpdatePortfolio } from './portfolios'
-import { onFetchProjects, onOpenProjectSucceeded, onProjectAdd, onProjectDelete } from './projects'
+import { onOpenProjectSucceeded } from './projects'
import {
onAddMachine,
onAddRackToTile,
@@ -23,29 +21,19 @@ import {
onDeleteRack,
onDeleteRoom,
onDeleteTile,
- onDeleteTopology,
onDeleteUnit,
onEditRackName,
onEditRoomName,
onStartNewRoomConstruction,
} from './topology'
-import { ADD_TOPOLOGY, DELETE_TOPOLOGY } from '../actions/topologies'
-import { ADD_SCENARIO, DELETE_SCENARIO, OPEN_SCENARIO_SUCCEEDED, UPDATE_SCENARIO } from '../actions/scenarios'
-import { onAddScenario, onDeleteScenario, onOpenScenarioSucceeded, onUpdateScenario } from './scenarios'
+import { ADD_TOPOLOGY } from '../actions/topologies'
import { onAddPrefab } from './prefabs'
import { ADD_PREFAB } from '../actions/prefabs'
export default function* rootSaga() {
- yield takeEvery(FETCH_PROJECTS, onFetchProjects)
- yield takeEvery(ADD_PROJECT, onProjectAdd)
- yield takeEvery(DELETE_PROJECT, onProjectDelete)
-
yield takeEvery(OPEN_PROJECT_SUCCEEDED, onOpenProjectSucceeded)
- yield takeEvery(OPEN_PORTFOLIO_SUCCEEDED, onOpenPortfolioSucceeded)
- yield takeEvery(OPEN_SCENARIO_SUCCEEDED, onOpenScenarioSucceeded)
yield takeEvery(ADD_TOPOLOGY, onAddTopology)
- yield takeEvery(DELETE_TOPOLOGY, onDeleteTopology)
yield takeEvery(START_NEW_ROOM_CONSTRUCTION, onStartNewRoomConstruction)
yield takeEvery(CANCEL_NEW_ROOM_CONSTRUCTION, onCancelNewRoomConstruction)
yield takeEvery(ADD_TILE, onAddTile)
@@ -60,13 +48,5 @@ export default function* rootSaga() {
yield takeEvery(ADD_UNIT, onAddUnit)
yield takeEvery(DELETE_UNIT, onDeleteUnit)
- yield takeEvery(ADD_PORTFOLIO, onAddPortfolio)
- yield takeEvery(UPDATE_PORTFOLIO, onUpdatePortfolio)
- yield takeEvery(DELETE_PORTFOLIO, onDeletePortfolio)
-
- yield takeEvery(ADD_SCENARIO, onAddScenario)
- yield takeEvery(UPDATE_SCENARIO, onUpdateScenario)
- yield takeEvery(DELETE_SCENARIO, onDeleteScenario)
-
yield takeEvery(ADD_PREFAB, onAddPrefab)
}
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js
index e5fd092d..9b4f8094 100644
--- a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js
+++ b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js
@@ -1,234 +1,36 @@
import { call, put, select, getContext } from 'redux-saga/effects'
-import { addToStore } from '../actions/objects'
-import { getAllSchedulers } from '../../api/schedulers'
-import { getProject } from '../../api/projects'
-import { getAllTraces } from '../../api/traces'
-import { getTopology, updateTopology } from '../../api/topologies'
-import { uuid } from 'uuidv4'
-
-export const OBJECT_SELECTORS = {
- project: (state) => state.objects.project,
- user: (state) => state.objects.user,
- authorization: (state) => state.objects.authorization,
- portfolio: (state) => state.objects.portfolio,
- scenario: (state) => state.objects.scenario,
- cpu: (state) => state.objects.cpu,
- gpu: (state) => state.objects.gpu,
- memory: (state) => state.objects.memory,
- storage: (state) => state.objects.storage,
- machine: (state) => state.objects.machine,
- rack: (state) => state.objects.rack,
- tile: (state) => state.objects.tile,
- room: (state) => state.objects.room,
- topology: (state) => state.objects.topology,
-}
-
-function* fetchAndStoreObject(objectType, id, apiCall) {
- const objectStore = yield select(OBJECT_SELECTORS[objectType])
- let object = objectStore[id]
- if (!object) {
- object = yield apiCall
- yield put(addToStore(objectType, object))
- }
- return object
-}
-
-function* fetchAndStoreObjects(objectType, apiCall) {
- const objects = yield apiCall
- for (let object of objects) {
- yield put(addToStore(objectType, object))
- }
- return objects
-}
-
-export const fetchAndStoreProject = function* (id) {
- const auth = yield getContext('auth')
- return yield fetchAndStoreObject('project', id, call(getProject, auth, id))
-}
-
+import { fetchTopology, updateTopology } from '../../api/topologies'
+import { Topology } from '../../util/topology-schema'
+import { denormalize, normalize } from 'normalizr'
+import { storeTopology } from '../actions/topologies'
+
+/**
+ * Fetches and normalizes the topology with the specified identifier.
+ */
export const fetchAndStoreTopology = function* (id) {
- const topologyStore = yield select(OBJECT_SELECTORS['topology'])
- const roomStore = yield select(OBJECT_SELECTORS['room'])
- const tileStore = yield select(OBJECT_SELECTORS['tile'])
- const rackStore = yield select(OBJECT_SELECTORS['rack'])
- const machineStore = yield select(OBJECT_SELECTORS['machine'])
const auth = yield getContext('auth')
- let topology = topologyStore[id]
+ let topology = yield select((state) => state.objects.topology[id])
if (!topology) {
- const fullTopology = yield call(getTopology, auth, id)
-
- for (let roomIdx in fullTopology.rooms) {
- const fullRoom = fullTopology.rooms[roomIdx]
-
- generateIdIfNotPresent(fullRoom)
-
- if (!roomStore[fullRoom._id]) {
- for (let tileIdx in fullRoom.tiles) {
- const fullTile = fullRoom.tiles[tileIdx]
-
- generateIdIfNotPresent(fullTile)
-
- if (!tileStore[fullTile._id]) {
- if (fullTile.rack) {
- const fullRack = fullTile.rack
-
- generateIdIfNotPresent(fullRack)
-
- if (!rackStore[fullRack._id]) {
- for (let machineIdx in fullRack.machines) {
- const fullMachine = fullRack.machines[machineIdx]
-
- generateIdIfNotPresent(fullMachine)
-
- if (!machineStore[fullMachine._id]) {
- let machine = (({ _id, position, cpus, gpus, memories, storages }) => ({
- _id,
- rackId: fullRack._id,
- position,
- cpuIds: cpus.map((u) => u._id),
- gpuIds: gpus.map((u) => u._id),
- memoryIds: memories.map((u) => u._id),
- storageIds: storages.map((u) => u._id),
- }))(fullMachine)
- yield put(addToStore('machine', machine))
- }
- }
-
- const filledSlots = new Array(fullRack.capacity).fill(null)
- fullRack.machines.forEach(
- (machine) => (filledSlots[machine.position - 1] = machine._id)
- )
- let rack = (({ _id, name, capacity, powerCapacityW }) => ({
- _id,
- name,
- capacity,
- powerCapacityW,
- machineIds: filledSlots,
- }))(fullRack)
- yield put(addToStore('rack', rack))
- }
- }
-
- let tile = (({ _id, positionX, positionY, rack }) => ({
- _id,
- roomId: fullRoom._id,
- positionX,
- positionY,
- rackId: rack ? rack._id : undefined,
- }))(fullTile)
- yield put(addToStore('tile', tile))
- }
- }
-
- let room = (({ _id, name, tiles }) => ({ _id, name, tileIds: tiles.map((t) => t._id) }))(fullRoom)
- yield put(addToStore('room', room))
- }
- }
-
- topology = (({ _id, name, rooms }) => ({ _id, name, roomIds: rooms.map((r) => r._id) }))(fullTopology)
- yield put(addToStore('topology', topology))
-
- // TODO consider pushing the IDs
+ const newTopology = yield call(fetchTopology, auth, id)
+ const { entities } = normalize(newTopology, Topology)
+ yield put(storeTopology(entities))
}
return topology
}
-const generateIdIfNotPresent = (obj) => {
- if (!obj._id) {
- obj._id = uuid()
- }
-}
-
export const updateTopologyOnServer = function* (id) {
- const topology = yield getTopologyAsObject(id, true)
+ const topology = yield denormalizeTopology(id)
const auth = yield getContext('auth')
yield call(updateTopology, auth, topology)
}
-export const getTopologyAsObject = function* (id, keepIds) {
- const topologyStore = yield select(OBJECT_SELECTORS['topology'])
- const rooms = yield getAllRooms(topologyStore[id].roomIds, keepIds)
- return {
- _id: keepIds ? id : undefined,
- name: topologyStore[id].name,
- rooms: rooms,
- }
-}
-
-export const getAllRooms = function* (roomIds, keepIds) {
- const roomStore = yield select(OBJECT_SELECTORS['room'])
-
- let rooms = []
-
- for (let id of roomIds) {
- let tiles = yield getAllRoomTiles(roomStore[id], keepIds)
- rooms.push({
- _id: keepIds ? id : undefined,
- name: roomStore[id].name,
- tiles: tiles,
- })
- }
- return rooms
-}
-
-export const getAllRoomTiles = function* (roomStore, keepIds) {
- let tiles = []
-
- for (let id of roomStore.tileIds) {
- tiles.push(yield getTileById(id, keepIds))
- }
- return tiles
-}
-
-export const getTileById = function* (id, keepIds) {
- const tileStore = yield select(OBJECT_SELECTORS['tile'])
- return {
- _id: keepIds ? id : undefined,
- positionX: tileStore[id].positionX,
- positionY: tileStore[id].positionY,
- rack: !tileStore[id].rackId ? undefined : yield getRackById(tileStore[id].rackId, keepIds),
- }
-}
-
-export const getRackById = function* (id, keepIds) {
- const rackStore = yield select(OBJECT_SELECTORS['rack'])
- const machineStore = yield select(OBJECT_SELECTORS['machine'])
- const cpuStore = yield select(OBJECT_SELECTORS['cpu'])
- const gpuStore = yield select(OBJECT_SELECTORS['gpu'])
- const memoryStore = yield select(OBJECT_SELECTORS['memory'])
- const storageStore = yield select(OBJECT_SELECTORS['storage'])
-
- return {
- _id: keepIds ? rackStore[id]._id : undefined,
- name: rackStore[id].name,
- capacity: rackStore[id].capacity,
- powerCapacityW: rackStore[id].powerCapacityW,
- machines: rackStore[id].machineIds
- .filter((m) => m !== null)
- .map((machineId) => ({
- _id: keepIds ? machineId : undefined,
- position: machineStore[machineId].position,
- cpus: machineStore[machineId].cpuIds.map((id) => cpuStore[id]),
- gpus: machineStore[machineId].gpuIds.map((id) => gpuStore[id]),
- memories: machineStore[machineId].memoryIds.map((id) => memoryStore[id]),
- storages: machineStore[machineId].storageIds.map((id) => storageStore[id]),
- })),
- }
-}
-
-export const fetchAndStoreAllTraces = function* () {
- const auth = yield getContext('auth')
- return yield fetchAndStoreObjects('trace', call(getAllTraces, auth))
-}
-
-export const fetchAndStoreAllSchedulers = function* () {
- const auth = yield getContext('auth')
- const objects = yield call(getAllSchedulers, auth)
- for (let object of objects) {
- object._id = object.name
- yield put(addToStore('scheduler', object))
- }
- return objects
+/**
+ * Denormalizes the topology representation in order to be stored on the server.
+ */
+export const denormalizeTopology = function* (id) {
+ const objects = yield select((state) => state.objects)
+ const topology = objects.topology[id]
+ return denormalize(topology, Topology, objects)
}
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js
deleted file mode 100644
index 340cb490..00000000
--- a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import { call, put, select, delay, getContext } from 'redux-saga/effects'
-import { addPropToStoreObject, addToStore } from '../actions/objects'
-import { addPortfolio, deletePortfolio, getPortfolio, updatePortfolio } from '../../api/portfolios'
-import { getProject } from '../../api/projects'
-import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
-import { fetchAndStoreAllTopologiesOfProject } from './topology'
-import { getScenario } from '../../api/scenarios'
-
-export function* onOpenPortfolioSucceeded(action) {
- try {
- const auth = yield getContext('auth')
- const project = yield call(getProject, auth, action.projectId)
- yield put(addToStore('project', project))
- yield fetchAndStoreAllTopologiesOfProject(project._id)
- yield fetchPortfoliosOfProject()
- yield fetchAndStoreAllSchedulers()
- yield fetchAndStoreAllTraces()
-
- yield watchForPortfolioResults()
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* watchForPortfolioResults() {
- try {
- const currentPortfolioId = yield select((state) => state.currentPortfolioId)
- let unfinishedScenarios = yield getCurrentUnfinishedScenarios()
-
- while (unfinishedScenarios.length > 0) {
- yield delay(3000)
- yield fetchPortfolioWithScenarios(currentPortfolioId)
- unfinishedScenarios = yield getCurrentUnfinishedScenarios()
- }
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* getCurrentUnfinishedScenarios() {
- try {
- const currentPortfolioId = yield select((state) => state.currentPortfolioId)
- const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds)
- const scenarioObjects = yield select((state) => state.objects.scenario)
- const scenarios = scenarioIds.map((s) => scenarioObjects[s])
- return scenarios.filter((s) => !s || s.simulation.state === 'QUEUED' || s.simulation.state === 'RUNNING')
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* fetchPortfoliosOfProject() {
- try {
- const currentProjectId = yield select((state) => state.currentProjectId)
- const currentProject = yield select((state) => state.objects.project[currentProjectId])
-
- yield fetchAndStoreAllSchedulers()
- yield fetchAndStoreAllTraces()
-
- for (let i in currentProject.portfolioIds) {
- yield fetchPortfolioWithScenarios(currentProject.portfolioIds[i])
- }
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* fetchPortfolioWithScenarios(portfolioId) {
- try {
- const auth = yield getContext('auth')
- const portfolio = yield call(getPortfolio, auth, portfolioId)
- yield put(addToStore('portfolio', portfolio))
-
- for (let i in portfolio.scenarioIds) {
- const scenario = yield call(getScenario, auth, portfolio.scenarioIds[i])
- yield put(addToStore('scenario', scenario))
- }
- return portfolio
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onAddPortfolio(action) {
- try {
- const currentProjectId = yield select((state) => state.currentProjectId)
- const auth = yield getContext('auth')
- const portfolio = yield call(
- addPortfolio,
- auth,
- currentProjectId,
- Object.assign({}, action.portfolio, {
- projectId: currentProjectId,
- scenarioIds: [],
- })
- )
- yield put(addToStore('portfolio', portfolio))
-
- const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds)
- yield put(
- addPropToStoreObject('project', currentProjectId, {
- portfolioIds: portfolioIds.concat([portfolio._id]),
- })
- )
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onUpdatePortfolio(action) {
- try {
- const auth = yield getContext('auth')
- const portfolio = yield call(updatePortfolio, auth, action.portfolio._id, action.portfolio)
- yield put(addToStore('portfolio', portfolio))
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onDeletePortfolio(action) {
- try {
- const auth = yield getContext('auth')
- yield call(deletePortfolio, auth, action.id)
-
- const currentProjectId = yield select((state) => state.currentProjectId)
- const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds)
-
- yield put(
- addPropToStoreObject('project', currentProjectId, {
- portfolioIds: portfolioIds.filter((id) => id !== action.id),
- })
- )
- } catch (error) {
- console.error(error)
- }
-}
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js b/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js
index ec679391..91b03bf6 100644
--- a/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js
+++ b/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js
@@ -1,14 +1,17 @@
import { call, put, select, getContext } from 'redux-saga/effects'
import { addToStore } from '../actions/objects'
import { addPrefab } from '../../api/prefabs'
-import { getRackById } from './objects'
+import { Rack } from '../../util/topology-schema'
+import { denormalize } from 'normalizr'
export function* onAddPrefab(action) {
try {
- const currentRackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId)
- const currentRackJson = yield getRackById(currentRackId, false)
+ const interactionLevel = yield select((state) => state.interactionLevel)
+ const objects = yield select((state) => state.objects)
+ const rack = objects.rack[objects.tile[interactionLevel.tileId].rack]
+ const prefabRack = denormalize(rack, Rack, objects)
const auth = yield getContext('auth')
- const prefab = yield call(addPrefab, auth, { name: action.name, rack: currentRackJson })
+ const prefab = yield call(() => addPrefab(auth, { name: action.name, rack: prefabRack }))
yield put(addToStore('prefab', prefab))
} catch (error) {
console.error(error)
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/projects.js b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js
index 506df6ed..5809d4d2 100644
--- a/opendc-web/opendc-web-ui/src/redux/sagas/projects.js
+++ b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js
@@ -1,52 +1,8 @@
-import { call, put, getContext } from 'redux-saga/effects'
-import { addToStore } from '../actions/objects'
-import { addProjectSucceeded, deleteProjectSucceeded, fetchProjectsSucceeded } from '../actions/projects'
-import { addProject, deleteProject, getProject, getProjects } from '../../api/projects'
import { fetchAndStoreAllTopologiesOfProject } from './topology'
-import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
-import { fetchPortfoliosOfProject } from './portfolios'
export function* onOpenProjectSucceeded(action) {
try {
- const auth = yield getContext('auth')
- const project = yield call(getProject, auth, action.id)
- yield put(addToStore('project', project))
-
yield fetchAndStoreAllTopologiesOfProject(action.id, true)
- yield fetchPortfoliosOfProject()
- yield fetchAndStoreAllSchedulers()
- yield fetchAndStoreAllTraces()
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onProjectAdd(action) {
- try {
- const auth = yield getContext('auth')
- const project = yield call(addProject, auth, { name: action.name })
- yield put(addToStore('project', project))
- yield put(addProjectSucceeded(project))
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onProjectDelete(action) {
- try {
- const auth = yield getContext('auth')
- yield call(deleteProject, auth, action.id)
- yield put(deleteProjectSucceeded(action.id))
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onFetchProjects(action) {
- try {
- const auth = yield getContext('auth')
- const projects = yield call(getProjects, auth)
- yield put(fetchProjectsSucceeded(projects))
} catch (error) {
console.error(error)
}
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js b/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js
deleted file mode 100644
index bdb7c45d..00000000
--- a/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { call, put, select, getContext } from 'redux-saga/effects'
-import { addPropToStoreObject, addToStore } from '../actions/objects'
-import { getProject } from '../../api/projects'
-import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
-import { fetchAndStoreAllTopologiesOfProject } from './topology'
-import { addScenario, deleteScenario, updateScenario } from '../../api/scenarios'
-import { fetchPortfolioWithScenarios, watchForPortfolioResults } from './portfolios'
-
-export function* onOpenScenarioSucceeded(action) {
- try {
- const auth = yield getContext('auth')
- const project = yield call(getProject, auth, action.projectId)
- yield put(addToStore('project', project))
- yield fetchAndStoreAllTopologiesOfProject(project._id)
- yield fetchAndStoreAllSchedulers()
- yield fetchAndStoreAllTraces()
- yield fetchPortfolioWithScenarios(action.portfolioId)
-
- // TODO Fetch scenario-specific metrics
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onAddScenario(action) {
- try {
- const auth = yield getContext('auth')
- const scenario = yield call(addScenario, auth, action.scenario.portfolioId, action.scenario)
- yield put(addToStore('scenario', scenario))
-
- const scenarioIds = yield select((state) => state.objects.portfolio[action.scenario.portfolioId].scenarioIds)
- yield put(
- addPropToStoreObject('portfolio', action.scenario.portfolioId, {
- scenarioIds: scenarioIds.concat([scenario._id]),
- })
- )
- yield watchForPortfolioResults()
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onUpdateScenario(action) {
- try {
- const auth = yield getContext('auth')
- const scenario = yield call(updateScenario, auth, action.scenario._id, action.scenario)
- yield put(addToStore('scenario', scenario))
- } catch (error) {
- console.error(error)
- }
-}
-
-export function* onDeleteScenario(action) {
- try {
- const auth = yield getContext('auth')
- yield call(deleteScenario, auth, action.id)
-
- const currentPortfolioId = yield select((state) => state.currentPortfolioId)
- const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds)
-
- yield put(
- addPropToStoreObject('scenario', currentPortfolioId, {
- scenarioIds: scenarioIds.filter((id) => id !== action.id),
- })
- )
- } catch (error) {
- console.error(error)
- }
-}
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
index e5fd3d39..5d9154fd 100644
--- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
+++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
@@ -16,16 +16,17 @@ import {
DEFAULT_RACK_SLOT_CAPACITY,
MAX_NUM_UNITS_PER_MACHINE,
} from '../../components/app/map/MapConstants'
-import { fetchAndStoreTopology, getTopologyAsObject, updateTopologyOnServer } from './objects'
+import { fetchAndStoreTopology, denormalizeTopology, updateTopologyOnServer } from './objects'
import { uuid } from 'uuidv4'
-import { addTopology, deleteTopology } from '../../api/topologies'
+import { addTopology } from '../../api/topologies'
export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = false) {
try {
- const project = yield select((state) => state.objects.project[projectId])
+ const queryClient = yield getContext('queryClient')
+ const project = yield call(() => queryClient.fetchQuery(['projects', projectId]))
- for (let i in project.topologyIds) {
- yield fetchAndStoreTopology(project.topologyIds[i])
+ for (const id of project.topologyIds) {
+ yield fetchAndStoreTopology(id)
}
if (setTopology) {
@@ -38,62 +39,26 @@ export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = fa
export function* onAddTopology(action) {
try {
- const currentProjectId = yield select((state) => state.currentProjectId)
+ const { projectId, duplicateId, name } = action
let topologyToBeCreated
- if (action.duplicateId) {
- topologyToBeCreated = yield getTopologyAsObject(action.duplicateId, false)
- topologyToBeCreated = Object.assign({}, topologyToBeCreated, {
- name: action.name,
- })
+ if (duplicateId) {
+ topologyToBeCreated = yield denormalizeTopology(duplicateId)
+ topologyToBeCreated = { ...topologyToBeCreated, name }
+ delete topologyToBeCreated._id
} else {
topologyToBeCreated = { name: action.name, rooms: [] }
}
const auth = yield getContext('auth')
- const topology = yield call(
- addTopology,
- auth,
- Object.assign({}, topologyToBeCreated, {
- projectId: currentProjectId,
- })
- )
+ const topology = yield call(addTopology, auth, { ...topologyToBeCreated, projectId })
yield fetchAndStoreTopology(topology._id)
-
- const topologyIds = yield select((state) => state.objects.project[currentProjectId].topologyIds)
- yield put(
- addPropToStoreObject('project', currentProjectId, {
- topologyIds: topologyIds.concat([topology._id]),
- })
- )
yield put(setCurrentTopology(topology._id))
} catch (error) {
console.error(error)
}
}
-export function* onDeleteTopology(action) {
- try {
- const currentProjectId = yield select((state) => state.currentProjectId)
- const topologyIds = yield select((state) => state.objects.project[currentProjectId].topologyIds)
- const currentTopologyId = yield select((state) => state.currentTopologyId)
- if (currentTopologyId === action.id) {
- yield put(setCurrentTopology(topologyIds.filter((t) => t !== action.id)[0]))
- }
-
- const auth = yield getContext('auth')
- yield call(deleteTopology, auth, action.id)
-
- yield put(
- addPropToStoreObject('project', currentProjectId, {
- topologyIds: topologyIds.filter((id) => id !== action.id),
- })
- )
- } catch (error) {
- console.error(error)
- }
-}
-
export function* onStartNewRoomConstruction() {
try {
const topologyId = yield select((state) => state.currentTopologyId)
@@ -101,10 +66,10 @@ export function* onStartNewRoomConstruction() {
_id: uuid(),
name: 'Room',
topologyId,
- tileIds: [],
+ tiles: [],
}
yield put(addToStore('room', room))
- yield put(addIdToStoreObjectListProp('topology', topologyId, 'roomIds', room._id))
+ yield put(addIdToStoreObjectListProp('topology', topologyId, 'rooms', room._id))
yield updateTopologyOnServer(topologyId)
yield put(startNewRoomConstructionSucceeded(room._id))
} catch (error) {
@@ -116,7 +81,7 @@ export function* onCancelNewRoomConstruction() {
try {
const topologyId = yield select((state) => state.currentTopologyId)
const roomId = yield select((state) => state.construction.currentRoomInConstruction)
- yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'roomIds', roomId))
+ yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'rooms', roomId))
// TODO remove room from store, too
yield updateTopologyOnServer(topologyId)
yield put(cancelNewRoomConstructionSucceeded())
@@ -136,7 +101,7 @@ export function* onAddTile(action) {
positionY: action.positionY,
}
yield put(addToStore('tile', tile))
- yield put(addIdToStoreObjectListProp('room', roomId, 'tileIds', tile._id))
+ yield put(addIdToStoreObjectListProp('room', roomId, 'tiles', tile._id))
yield updateTopologyOnServer(topologyId)
} catch (error) {
console.error(error)
@@ -147,7 +112,7 @@ export function* onDeleteTile(action) {
try {
const topologyId = yield select((state) => state.currentTopologyId)
const roomId = yield select((state) => state.construction.currentRoomInConstruction)
- yield put(removeIdFromStoreObjectListProp('room', roomId, 'tileIds', action.tileId))
+ yield put(removeIdFromStoreObjectListProp('room', roomId, 'tiles', action.tileId))
yield updateTopologyOnServer(topologyId)
} catch (error) {
console.error(error)
@@ -172,7 +137,7 @@ export function* onDeleteRoom() {
const topologyId = yield select((state) => state.currentTopologyId)
const roomId = yield select((state) => state.interactionLevel.roomId)
yield put(goDownOneInteractionLevel())
- yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'roomIds', roomId))
+ yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'rooms', roomId))
yield updateTopologyOnServer(topologyId)
} catch (error) {
console.error(error)
@@ -182,7 +147,7 @@ export function* onDeleteRoom() {
export function* onEditRackName(action) {
try {
const topologyId = yield select((state) => state.currentTopologyId)
- const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId)
+ const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rack)
const rack = Object.assign({}, yield select((state) => state.objects.rack[rackId]))
rack.name = action.name
yield put(addPropToStoreObject('rack', rackId, { name: action.name }))
@@ -197,7 +162,7 @@ export function* onDeleteRack() {
const topologyId = yield select((state) => state.currentTopologyId)
const tileId = yield select((state) => state.interactionLevel.tileId)
yield put(goDownOneInteractionLevel())
- yield put(addPropToStoreObject('tile', tileId, { rackId: undefined }))
+ yield put(addPropToStoreObject('tile', tileId, { rack: undefined }))
yield updateTopologyOnServer(topologyId)
} catch (error) {
console.error(error)
@@ -212,10 +177,10 @@ export function* onAddRackToTile(action) {
name: 'Rack',
capacity: DEFAULT_RACK_SLOT_CAPACITY,
powerCapacityW: DEFAULT_RACK_POWER_CAPACITY,
+ machines: [],
}
- rack.machineIds = new Array(rack.capacity).fill(null)
yield put(addToStore('rack', rack))
- yield put(addPropToStoreObject('tile', action.tileId, { rackId: rack._id }))
+ yield put(addPropToStoreObject('tile', action.tileId, { rack: rack._id }))
yield updateTopologyOnServer(topologyId)
} catch (error) {
console.error(error)
@@ -225,23 +190,21 @@ export function* onAddRackToTile(action) {
export function* onAddMachine(action) {
try {
const topologyId = yield select((state) => state.currentTopologyId)
- const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId)
+ const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rack)
const rack = yield select((state) => state.objects.rack[rackId])
const machine = {
_id: uuid(),
- rackId,
position: action.position,
- cpuIds: [],
- gpuIds: [],
- memoryIds: [],
- storageIds: [],
+ cpus: [],
+ gpus: [],
+ memories: [],
+ storages: [],
}
yield put(addToStore('machine', machine))
- const machineIds = [...rack.machineIds]
- machineIds[machine.position - 1] = machine._id
- yield put(addPropToStoreObject('rack', rackId, { machineIds }))
+ const machineIds = [...rack.machines, machine._id]
+ yield put(addPropToStoreObject('rack', rackId, { machines: machineIds }))
yield updateTopologyOnServer(topologyId)
} catch (error) {
console.error(error)
@@ -253,35 +216,41 @@ export function* onDeleteMachine() {
const topologyId = yield select((state) => state.currentTopologyId)
const tileId = yield select((state) => state.interactionLevel.tileId)
const position = yield select((state) => state.interactionLevel.position)
- const rack = yield select((state) => state.objects.rack[state.objects.tile[tileId].rackId])
- const machineIds = [...rack.machineIds]
- machineIds[position - 1] = null
+ const rack = yield select((state) => state.objects.rack[state.objects.tile[tileId].rack])
yield put(goDownOneInteractionLevel())
- yield put(addPropToStoreObject('rack', rack._id, { machineIds }))
+ yield put(
+ addPropToStoreObject('rack', rack._id, { machines: rack.machines.filter((_, idx) => idx !== position - 1) })
+ )
yield updateTopologyOnServer(topologyId)
} catch (error) {
console.error(error)
}
}
+const unitMapping = {
+ cpu: 'cpus',
+ gpu: 'gpus',
+ memory: 'memories',
+ storage: 'storages',
+}
+
export function* onAddUnit(action) {
try {
const topologyId = yield select((state) => state.currentTopologyId)
const tileId = yield select((state) => state.interactionLevel.tileId)
const position = yield select((state) => state.interactionLevel.position)
const machine = yield select(
- (state) =>
- state.objects.machine[state.objects.rack[state.objects.tile[tileId].rackId].machineIds[position - 1]]
+ (state) => state.objects.machine[state.objects.rack[state.objects.tile[tileId].rack].machines[position - 1]]
)
- if (machine[action.unitType + 'Ids'].length >= MAX_NUM_UNITS_PER_MACHINE) {
+ if (machine[unitMapping[action.unitType]].length >= MAX_NUM_UNITS_PER_MACHINE) {
return
}
- const units = [...machine[action.unitType + 'Ids'], action.id]
+ const units = [...machine[unitMapping[action.unitType]], action.id]
yield put(
addPropToStoreObject('machine', machine._id, {
- [action.unitType + 'Ids']: units,
+ [unitMapping[action.unitType]]: units,
})
)
yield updateTopologyOnServer(topologyId)
@@ -296,15 +265,14 @@ export function* onDeleteUnit(action) {
const tileId = yield select((state) => state.interactionLevel.tileId)
const position = yield select((state) => state.interactionLevel.position)
const machine = yield select(
- (state) =>
- state.objects.machine[state.objects.rack[state.objects.tile[tileId].rackId].machineIds[position - 1]]
+ (state) => state.objects.machine[state.objects.rack[state.objects.tile[tileId].rack].machines[position - 1]]
)
- const unitIds = machine[action.unitType + 'Ids'].slice()
+ const unitIds = machine[unitMapping[action.unitType]].slice()
unitIds.splice(action.index, 1)
yield put(
addPropToStoreObject('machine', machine._id, {
- [action.unitType + 'Ids']: unitIds,
+ [unitMapping[action.unitType]]: unitIds,
})
)
yield updateTopologyOnServer(topologyId)
diff --git a/opendc-web/opendc-web-ui/src/shapes.js b/opendc-web/opendc-web-ui/src/shapes.js
index 6c29eab0..3c27ad11 100644
--- a/opendc-web/opendc-web-ui/src/shapes.js
+++ b/opendc-web/opendc-web-ui/src/shapes.js
@@ -40,14 +40,6 @@ export const Project = PropTypes.shape({
portfolioIds: PropTypes.array.isRequired,
})
-export const Authorization = PropTypes.shape({
- userId: PropTypes.string.isRequired,
- user: User,
- projectId: PropTypes.string.isRequired,
- project: Project,
- level: PropTypes.string.isRequired,
-})
-
export const ProcessingUnit = PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
@@ -66,44 +58,37 @@ export const StorageUnit = PropTypes.shape({
export const Machine = PropTypes.shape({
_id: PropTypes.string.isRequired,
- rackId: PropTypes.string.isRequired,
position: PropTypes.number.isRequired,
- cpuIds: PropTypes.arrayOf(PropTypes.string.isRequired),
- cpus: PropTypes.arrayOf(ProcessingUnit),
- gpuIds: PropTypes.arrayOf(PropTypes.string.isRequired),
- gpus: PropTypes.arrayOf(ProcessingUnit),
- memoryIds: PropTypes.arrayOf(PropTypes.string.isRequired),
- memories: PropTypes.arrayOf(StorageUnit),
- storageIds: PropTypes.arrayOf(PropTypes.string.isRequired),
- storages: PropTypes.arrayOf(StorageUnit),
+ cpus: PropTypes.arrayOf(PropTypes.string),
+ gpus: PropTypes.arrayOf(PropTypes.string),
+ memories: PropTypes.arrayOf(PropTypes.string),
+ storages: PropTypes.arrayOf(PropTypes.string),
})
export const Rack = PropTypes.shape({
_id: PropTypes.string.isRequired,
capacity: PropTypes.number.isRequired,
powerCapacityW: PropTypes.number.isRequired,
- machines: PropTypes.arrayOf(Machine),
+ machines: PropTypes.arrayOf(PropTypes.string),
})
export const Tile = PropTypes.shape({
_id: PropTypes.string.isRequired,
- roomId: PropTypes.string.isRequired,
positionX: PropTypes.number.isRequired,
positionY: PropTypes.number.isRequired,
- rackId: PropTypes.string,
- rack: Rack,
+ rack: PropTypes.string,
})
export const Room = PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
- tiles: PropTypes.arrayOf(Tile),
+ tiles: PropTypes.arrayOf(PropTypes.string),
})
export const Topology = PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
- rooms: PropTypes.arrayOf(Room),
+ rooms: PropTypes.arrayOf(PropTypes.string),
})
export const Scheduler = PropTypes.shape({
diff --git a/opendc-web/opendc-web-ui/src/util/topology-schema.js b/opendc-web/opendc-web-ui/src/util/topology-schema.js
new file mode 100644
index 00000000..9acd688b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/util/topology-schema.js
@@ -0,0 +1,47 @@
+/*
+ * 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 { schema } from 'normalizr'
+
+const Cpu = new schema.Entity('cpu', {}, { idAttribute: '_id' })
+const Gpu = new schema.Entity('gpu', {}, { idAttribute: '_id' })
+const Memory = new schema.Entity('memory', {}, { idAttribute: '_id' })
+const Storage = new schema.Entity('storage', {}, { idAttribute: '_id' })
+
+export const Machine = new schema.Entity(
+ 'machine',
+ {
+ cpus: [Cpu],
+ gpus: [Gpu],
+ memories: [Memory],
+ storages: [Storage],
+ },
+ { idAttribute: '_id' }
+)
+
+export const Rack = new schema.Entity('rack', { machines: [Machine] }, { idAttribute: '_id' })
+
+export const Tile = new schema.Entity('tile', { rack: Rack }, { idAttribute: '_id' })
+
+export const Room = new schema.Entity('room', { tiles: [Tile] }, { idAttribute: '_id' })
+
+export const Topology = new schema.Entity('topology', { rooms: [Room] }, { idAttribute: '_id' })