summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src/sagas
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web/opendc-web-ui/src/sagas')
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/index.js80
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/objects.js229
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/portfolios.js131
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/prefabs.js15
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/profile.js12
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/projects.js48
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/scenarios.js65
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/topology.js311
-rw-r--r--opendc-web/opendc-web-ui/src/sagas/users.js44
9 files changed, 935 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-ui/src/sagas/index.js b/opendc-web/opendc-web-ui/src/sagas/index.js
new file mode 100644
index 00000000..6332b2fb
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/index.js
@@ -0,0 +1,80 @@
+import { takeEvery } from 'redux-saga/effects'
+import { LOG_IN } from '../actions/auth'
+import { ADD_PORTFOLIO, DELETE_PORTFOLIO, OPEN_PORTFOLIO_SUCCEEDED, UPDATE_PORTFOLIO } from '../actions/portfolios'
+import { ADD_PROJECT, DELETE_PROJECT, OPEN_PROJECT_SUCCEEDED } from '../actions/projects'
+import {
+ ADD_TILE,
+ CANCEL_NEW_ROOM_CONSTRUCTION,
+ DELETE_TILE,
+ START_NEW_ROOM_CONSTRUCTION,
+} from '../actions/topology/building'
+import { ADD_UNIT, DELETE_MACHINE, DELETE_UNIT } from '../actions/topology/machine'
+import { ADD_MACHINE, DELETE_RACK, EDIT_RACK_NAME } from '../actions/topology/rack'
+import { ADD_RACK_TO_TILE, DELETE_ROOM, EDIT_ROOM_NAME } from '../actions/topology/room'
+import { DELETE_CURRENT_USER, FETCH_AUTHORIZATIONS_OF_CURRENT_USER } from '../actions/users'
+import { onAddPortfolio, onDeletePortfolio, onOpenPortfolioSucceeded, onUpdatePortfolio } from './portfolios'
+import { onDeleteCurrentUser } from './profile'
+import { onOpenProjectSucceeded, onProjectAdd, onProjectDelete } from './projects'
+import {
+ onAddMachine,
+ onAddRackToTile,
+ onAddTile,
+ onAddTopology,
+ onAddUnit,
+ onCancelNewRoomConstruction,
+ onDeleteMachine,
+ onDeleteRack,
+ onDeleteRoom,
+ onDeleteTile,
+ onDeleteTopology,
+ onDeleteUnit,
+ onEditRackName,
+ onEditRoomName,
+ onStartNewRoomConstruction,
+} from './topology'
+import { onFetchAuthorizationsOfCurrentUser, onFetchLoggedInUser } from './users'
+import { ADD_TOPOLOGY, DELETE_TOPOLOGY } from '../actions/topologies'
+import { ADD_SCENARIO, DELETE_SCENARIO, OPEN_SCENARIO_SUCCEEDED, UPDATE_SCENARIO } from '../actions/scenarios'
+import { onAddScenario, onDeleteScenario, onOpenScenarioSucceeded, onUpdateScenario } from './scenarios'
+import { onAddPrefab } from './prefabs'
+import { ADD_PREFAB } from '../actions/prefabs'
+
+export default function* rootSaga() {
+ yield takeEvery(LOG_IN, onFetchLoggedInUser)
+
+ yield takeEvery(FETCH_AUTHORIZATIONS_OF_CURRENT_USER, onFetchAuthorizationsOfCurrentUser)
+ yield takeEvery(ADD_PROJECT, onProjectAdd)
+ yield takeEvery(DELETE_PROJECT, onProjectDelete)
+
+ yield takeEvery(DELETE_CURRENT_USER, onDeleteCurrentUser)
+
+ yield takeEvery(OPEN_PROJECT_SUCCEEDED, onOpenProjectSucceeded)
+ yield takeEvery(OPEN_PORTFOLIO_SUCCEEDED, onOpenPortfolioSucceeded)
+ yield takeEvery(OPEN_SCENARIO_SUCCEEDED, onOpenScenarioSucceeded)
+
+ yield takeEvery(ADD_TOPOLOGY, onAddTopology)
+ yield takeEvery(DELETE_TOPOLOGY, onDeleteTopology)
+ yield takeEvery(START_NEW_ROOM_CONSTRUCTION, onStartNewRoomConstruction)
+ yield takeEvery(CANCEL_NEW_ROOM_CONSTRUCTION, onCancelNewRoomConstruction)
+ yield takeEvery(ADD_TILE, onAddTile)
+ yield takeEvery(DELETE_TILE, onDeleteTile)
+ yield takeEvery(EDIT_ROOM_NAME, onEditRoomName)
+ yield takeEvery(DELETE_ROOM, onDeleteRoom)
+ yield takeEvery(EDIT_RACK_NAME, onEditRackName)
+ yield takeEvery(DELETE_RACK, onDeleteRack)
+ yield takeEvery(ADD_RACK_TO_TILE, onAddRackToTile)
+ yield takeEvery(ADD_MACHINE, onAddMachine)
+ yield takeEvery(DELETE_MACHINE, onDeleteMachine)
+ yield takeEvery(ADD_UNIT, onAddUnit)
+ yield takeEvery(DELETE_UNIT, onDeleteUnit)
+
+ yield takeEvery(ADD_PORTFOLIO, onAddPortfolio)
+ yield takeEvery(UPDATE_PORTFOLIO, onUpdatePortfolio)
+ yield takeEvery(DELETE_PORTFOLIO, onDeletePortfolio)
+
+ yield takeEvery(ADD_SCENARIO, onAddScenario)
+ yield takeEvery(UPDATE_SCENARIO, onUpdateScenario)
+ yield takeEvery(DELETE_SCENARIO, onDeleteScenario)
+
+ yield takeEvery(ADD_PREFAB, onAddPrefab)
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/objects.js b/opendc-web/opendc-web-ui/src/sagas/objects.js
new file mode 100644
index 00000000..313d9976
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/objects.js
@@ -0,0 +1,229 @@
+import { call, put, select } from 'redux-saga/effects'
+import { addToStore } from '../actions/objects'
+import { getAllSchedulers } from '../api/routes/schedulers'
+import { getProject } from '../api/routes/projects'
+import { getAllTraces } from '../api/routes/traces'
+import { getUser } from '../api/routes/users'
+import { getTopology, updateTopology } from '../api/routes/topologies'
+import { uuid } from 'uuidv4'
+
+export const OBJECT_SELECTORS = {
+ project: (state) => state.objects.project,
+ user: (state) => state.objects.user,
+ authorization: (state) => state.objects.authorization,
+ portfolio: (state) => state.objects.portfolio,
+ scenario: (state) => state.objects.scenario,
+ cpu: (state) => state.objects.cpu,
+ gpu: (state) => state.objects.gpu,
+ memory: (state) => state.objects.memory,
+ storage: (state) => state.objects.storage,
+ machine: (state) => state.objects.machine,
+ rack: (state) => state.objects.rack,
+ tile: (state) => state.objects.tile,
+ room: (state) => state.objects.room,
+ topology: (state) => state.objects.topology,
+}
+
+function* fetchAndStoreObject(objectType, id, apiCall) {
+ const objectStore = yield select(OBJECT_SELECTORS[objectType])
+ let object = objectStore[id]
+ if (!object) {
+ object = yield apiCall
+ yield put(addToStore(objectType, object))
+ }
+ return object
+}
+
+function* fetchAndStoreObjects(objectType, apiCall) {
+ const objects = yield apiCall
+ for (let object of objects) {
+ yield put(addToStore(objectType, object))
+ }
+ return objects
+}
+
+export const fetchAndStoreProject = (id) => fetchAndStoreObject('project', id, call(getProject, id))
+
+export const fetchAndStoreUser = (id) => fetchAndStoreObject('user', id, call(getUser, id))
+
+export const fetchAndStoreTopology = function* (id) {
+ const topologyStore = yield select(OBJECT_SELECTORS['topology'])
+ const roomStore = yield select(OBJECT_SELECTORS['room'])
+ const tileStore = yield select(OBJECT_SELECTORS['tile'])
+ const rackStore = yield select(OBJECT_SELECTORS['rack'])
+ const machineStore = yield select(OBJECT_SELECTORS['machine'])
+
+ let topology = topologyStore[id]
+ if (!topology) {
+ const fullTopology = yield call(getTopology, id)
+
+ for (let roomIdx in fullTopology.rooms) {
+ const fullRoom = fullTopology.rooms[roomIdx]
+
+ generateIdIfNotPresent(fullRoom)
+
+ if (!roomStore[fullRoom._id]) {
+ for (let tileIdx in fullRoom.tiles) {
+ const fullTile = fullRoom.tiles[tileIdx]
+
+ generateIdIfNotPresent(fullTile)
+
+ if (!tileStore[fullTile._id]) {
+ if (fullTile.rack) {
+ const fullRack = fullTile.rack
+
+ generateIdIfNotPresent(fullRack)
+
+ if (!rackStore[fullRack._id]) {
+ for (let machineIdx in fullRack.machines) {
+ const fullMachine = fullRack.machines[machineIdx]
+
+ generateIdIfNotPresent(fullMachine)
+
+ if (!machineStore[fullMachine._id]) {
+ let machine = (({ _id, position, cpus, gpus, memories, storages }) => ({
+ _id,
+ rackId: fullRack._id,
+ position,
+ cpuIds: cpus.map((u) => u._id),
+ gpuIds: gpus.map((u) => u._id),
+ memoryIds: memories.map((u) => u._id),
+ storageIds: storages.map((u) => u._id),
+ }))(fullMachine)
+ yield put(addToStore('machine', machine))
+ }
+ }
+
+ const filledSlots = new Array(fullRack.capacity).fill(null)
+ fullRack.machines.forEach(
+ (machine) => (filledSlots[machine.position - 1] = machine._id)
+ )
+ let rack = (({ _id, name, capacity, powerCapacityW }) => ({
+ _id,
+ name,
+ capacity,
+ powerCapacityW,
+ machineIds: filledSlots,
+ }))(fullRack)
+ yield put(addToStore('rack', rack))
+ }
+ }
+
+ let tile = (({ _id, positionX, positionY, rack }) => ({
+ _id,
+ roomId: fullRoom._id,
+ positionX,
+ positionY,
+ rackId: rack ? rack._id : undefined,
+ }))(fullTile)
+ yield put(addToStore('tile', tile))
+ }
+ }
+
+ let room = (({ _id, name, tiles }) => ({ _id, name, tileIds: tiles.map((t) => t._id) }))(fullRoom)
+ yield put(addToStore('room', room))
+ }
+ }
+
+ topology = (({ _id, name, rooms }) => ({ _id, name, roomIds: rooms.map((r) => r._id) }))(fullTopology)
+ yield put(addToStore('topology', topology))
+
+ // TODO consider pushing the IDs
+ }
+
+ return topology
+}
+
+const generateIdIfNotPresent = (obj) => {
+ if (!obj._id) {
+ obj._id = uuid()
+ }
+}
+
+export const updateTopologyOnServer = function* (id) {
+ const topology = yield getTopologyAsObject(id, true)
+
+ yield call(updateTopology, topology)
+}
+
+export const getTopologyAsObject = function* (id, keepIds) {
+ const topologyStore = yield select(OBJECT_SELECTORS['topology'])
+ const rooms = yield getAllRooms(topologyStore[id].roomIds, keepIds)
+ return {
+ _id: keepIds ? id : undefined,
+ name: topologyStore[id].name,
+ rooms: rooms,
+ }
+}
+
+export const getAllRooms = function* (roomIds, keepIds) {
+ const roomStore = yield select(OBJECT_SELECTORS['room'])
+
+ let rooms = []
+
+ for (let id of roomIds) {
+ let tiles = yield getAllRoomTiles(roomStore[id], keepIds)
+ rooms.push({
+ _id: keepIds ? id : undefined,
+ name: roomStore[id].name,
+ tiles: tiles,
+ })
+ }
+ return rooms
+}
+
+export const getAllRoomTiles = function* (roomStore, keepIds) {
+ let tiles = []
+
+ for (let id of roomStore.tileIds) {
+ tiles.push(yield getTileById(id, keepIds))
+ }
+ return tiles
+}
+
+export const getTileById = function* (id, keepIds) {
+ const tileStore = yield select(OBJECT_SELECTORS['tile'])
+ return {
+ _id: keepIds ? id : undefined,
+ positionX: tileStore[id].positionX,
+ positionY: tileStore[id].positionY,
+ rack: !tileStore[id].rackId ? undefined : yield getRackById(tileStore[id].rackId, keepIds),
+ }
+}
+
+export const getRackById = function* (id, keepIds) {
+ const rackStore = yield select(OBJECT_SELECTORS['rack'])
+ const machineStore = yield select(OBJECT_SELECTORS['machine'])
+ const cpuStore = yield select(OBJECT_SELECTORS['cpu'])
+ const gpuStore = yield select(OBJECT_SELECTORS['gpu'])
+ const memoryStore = yield select(OBJECT_SELECTORS['memory'])
+ const storageStore = yield select(OBJECT_SELECTORS['storage'])
+
+ return {
+ _id: keepIds ? rackStore[id]._id : undefined,
+ name: rackStore[id].name,
+ capacity: rackStore[id].capacity,
+ powerCapacityW: rackStore[id].powerCapacityW,
+ machines: rackStore[id].machineIds
+ .filter((m) => m !== null)
+ .map((machineId) => ({
+ _id: keepIds ? machineId : undefined,
+ position: machineStore[machineId].position,
+ cpus: machineStore[machineId].cpuIds.map((id) => cpuStore[id]),
+ gpus: machineStore[machineId].gpuIds.map((id) => gpuStore[id]),
+ memories: machineStore[machineId].memoryIds.map((id) => memoryStore[id]),
+ storages: machineStore[machineId].storageIds.map((id) => storageStore[id]),
+ })),
+ }
+}
+
+export const fetchAndStoreAllTraces = () => fetchAndStoreObjects('trace', call(getAllTraces))
+
+export const fetchAndStoreAllSchedulers = function* () {
+ const objects = yield call(getAllSchedulers)
+ for (let object of objects) {
+ object._id = object.name
+ yield put(addToStore('scheduler', object))
+ }
+ return objects
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/portfolios.js b/opendc-web/opendc-web-ui/src/sagas/portfolios.js
new file mode 100644
index 00000000..ed9bfd29
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/portfolios.js
@@ -0,0 +1,131 @@
+import { call, put, select, delay } from 'redux-saga/effects'
+import { addPropToStoreObject, addToStore } from '../actions/objects'
+import { addPortfolio, deletePortfolio, getPortfolio, updatePortfolio } from '../api/routes/portfolios'
+import { getProject } from '../api/routes/projects'
+import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
+import { fetchAndStoreAllTopologiesOfProject } from './topology'
+import { getScenario } from '../api/routes/scenarios'
+
+export function* onOpenPortfolioSucceeded(action) {
+ try {
+ const project = yield call(getProject, action.projectId)
+ yield put(addToStore('project', project))
+ yield fetchAndStoreAllTopologiesOfProject(project._id)
+ yield fetchPortfoliosOfProject()
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+
+ yield watchForPortfolioResults()
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* watchForPortfolioResults() {
+ try {
+ const currentPortfolioId = yield select((state) => state.currentPortfolioId)
+ let unfinishedScenarios = yield getCurrentUnfinishedScenarios()
+
+ while (unfinishedScenarios.length > 0) {
+ yield delay(3000)
+ yield fetchPortfolioWithScenarios(currentPortfolioId)
+ unfinishedScenarios = yield getCurrentUnfinishedScenarios()
+ }
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* getCurrentUnfinishedScenarios() {
+ try {
+ const currentPortfolioId = yield select((state) => state.currentPortfolioId)
+ const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds)
+ const scenarioObjects = yield select((state) => state.objects.scenario)
+ const scenarios = scenarioIds.map((s) => scenarioObjects[s])
+ return scenarios.filter((s) => !s || s.simulation.state === 'QUEUED' || s.simulation.state === 'RUNNING')
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* fetchPortfoliosOfProject() {
+ try {
+ const currentProjectId = yield select((state) => state.currentProjectId)
+ const currentProject = yield select((state) => state.objects.project[currentProjectId])
+
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+
+ for (let i in currentProject.portfolioIds) {
+ yield fetchPortfolioWithScenarios(currentProject.portfolioIds[i])
+ }
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* fetchPortfolioWithScenarios(portfolioId) {
+ try {
+ const portfolio = yield call(getPortfolio, portfolioId)
+ yield put(addToStore('portfolio', portfolio))
+
+ for (let i in portfolio.scenarioIds) {
+ const scenario = yield call(getScenario, portfolio.scenarioIds[i])
+ yield put(addToStore('scenario', scenario))
+ }
+ return portfolio
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddPortfolio(action) {
+ try {
+ const currentProjectId = yield select((state) => state.currentProjectId)
+
+ const portfolio = yield call(
+ addPortfolio,
+ currentProjectId,
+ Object.assign({}, action.portfolio, {
+ projectId: currentProjectId,
+ scenarioIds: [],
+ })
+ )
+ yield put(addToStore('portfolio', portfolio))
+
+ const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds)
+ yield put(
+ addPropToStoreObject('project', currentProjectId, {
+ portfolioIds: portfolioIds.concat([portfolio._id]),
+ })
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onUpdatePortfolio(action) {
+ try {
+ const portfolio = yield call(updatePortfolio, action.portfolio._id, action.portfolio)
+ yield put(addToStore('portfolio', portfolio))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeletePortfolio(action) {
+ try {
+ yield call(deletePortfolio, action.id)
+
+ const currentProjectId = yield select((state) => state.currentProjectId)
+ const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds)
+
+ yield put(
+ addPropToStoreObject('project', currentProjectId, {
+ portfolioIds: portfolioIds.filter((id) => id !== action.id),
+ })
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/prefabs.js b/opendc-web/opendc-web-ui/src/sagas/prefabs.js
new file mode 100644
index 00000000..16cf3d62
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/prefabs.js
@@ -0,0 +1,15 @@
+import { call, put, select } from 'redux-saga/effects'
+import { addToStore } from '../actions/objects'
+import { addPrefab } from '../api/routes/prefabs'
+import { getRackById } from './objects'
+
+export function* onAddPrefab(action) {
+ try {
+ const currentRackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId)
+ const currentRackJson = yield getRackById(currentRackId, false)
+ const prefab = yield call(addPrefab, { name: action.name, rack: currentRackJson })
+ yield put(addToStore('prefab', prefab))
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/profile.js b/opendc-web/opendc-web-ui/src/sagas/profile.js
new file mode 100644
index 00000000..e914ba56
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/profile.js
@@ -0,0 +1,12 @@
+import { call, put } from 'redux-saga/effects'
+import { deleteCurrentUserSucceeded } from '../actions/users'
+import { deleteUser } from '../api/routes/users'
+
+export function* onDeleteCurrentUser(action) {
+ try {
+ yield call(deleteUser, action.userId)
+ yield put(deleteCurrentUserSucceeded())
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/projects.js b/opendc-web/opendc-web-ui/src/sagas/projects.js
new file mode 100644
index 00000000..fdeea132
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/projects.js
@@ -0,0 +1,48 @@
+import { call, put } from 'redux-saga/effects'
+import { addToStore } from '../actions/objects'
+import { addProjectSucceeded, deleteProjectSucceeded } from '../actions/projects'
+import { addProject, deleteProject, getProject } from '../api/routes/projects'
+import { fetchAndStoreAllTopologiesOfProject } from './topology'
+import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
+import { fetchPortfoliosOfProject } from './portfolios'
+
+export function* onOpenProjectSucceeded(action) {
+ try {
+ const project = yield call(getProject, action.id)
+ yield put(addToStore('project', project))
+
+ yield fetchAndStoreAllTopologiesOfProject(action.id, true)
+ yield fetchPortfoliosOfProject()
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onProjectAdd(action) {
+ try {
+ const project = yield call(addProject, { name: action.name })
+ yield put(addToStore('project', project))
+
+ const authorization = {
+ projectId: project._id,
+ userId: action.userId,
+ authorizationLevel: 'OWN',
+ project,
+ }
+ yield put(addToStore('authorization', authorization))
+ yield put(addProjectSucceeded([authorization.userId, authorization.projectId]))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onProjectDelete(action) {
+ try {
+ yield call(deleteProject, action.id)
+ yield put(deleteProjectSucceeded(action.id))
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/scenarios.js b/opendc-web/opendc-web-ui/src/sagas/scenarios.js
new file mode 100644
index 00000000..59223610
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/scenarios.js
@@ -0,0 +1,65 @@
+import { call, put, select } from 'redux-saga/effects'
+import { addPropToStoreObject, addToStore } from '../actions/objects'
+import { getProject } from '../api/routes/projects'
+import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects'
+import { fetchAndStoreAllTopologiesOfProject } from './topology'
+import { addScenario, deleteScenario, updateScenario } from '../api/routes/scenarios'
+import { fetchPortfolioWithScenarios, watchForPortfolioResults } from './portfolios'
+
+export function* onOpenScenarioSucceeded(action) {
+ try {
+ const project = yield call(getProject, action.projectId)
+ yield put(addToStore('project', project))
+ yield fetchAndStoreAllTopologiesOfProject(project._id)
+ yield fetchAndStoreAllSchedulers()
+ yield fetchAndStoreAllTraces()
+ yield fetchPortfolioWithScenarios(action.portfolioId)
+
+ // TODO Fetch scenario-specific metrics
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddScenario(action) {
+ try {
+ const scenario = yield call(addScenario, action.scenario.portfolioId, action.scenario)
+ yield put(addToStore('scenario', scenario))
+
+ const scenarioIds = yield select((state) => state.objects.portfolio[action.scenario.portfolioId].scenarioIds)
+ yield put(
+ addPropToStoreObject('portfolio', action.scenario.portfolioId, {
+ scenarioIds: scenarioIds.concat([scenario._id]),
+ })
+ )
+ yield watchForPortfolioResults()
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onUpdateScenario(action) {
+ try {
+ const scenario = yield call(updateScenario, action.scenario._id, action.scenario)
+ yield put(addToStore('scenario', scenario))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteScenario(action) {
+ try {
+ yield call(deleteScenario, action.id)
+
+ const currentPortfolioId = yield select((state) => state.currentPortfolioId)
+ const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds)
+
+ yield put(
+ addPropToStoreObject('scenario', currentPortfolioId, {
+ scenarioIds: scenarioIds.filter((id) => id !== action.id),
+ })
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/topology.js b/opendc-web/opendc-web-ui/src/sagas/topology.js
new file mode 100644
index 00000000..bba1ebb1
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/topology.js
@@ -0,0 +1,311 @@
+import { call, put, select } from 'redux-saga/effects'
+import { goDownOneInteractionLevel } from '../actions/interaction-level'
+import {
+ addIdToStoreObjectListProp,
+ addPropToStoreObject,
+ addToStore,
+ removeIdFromStoreObjectListProp,
+} from '../actions/objects'
+import {
+ cancelNewRoomConstructionSucceeded,
+ setCurrentTopology,
+ startNewRoomConstructionSucceeded,
+} from '../actions/topology/building'
+import {
+ DEFAULT_RACK_POWER_CAPACITY,
+ DEFAULT_RACK_SLOT_CAPACITY,
+ MAX_NUM_UNITS_PER_MACHINE,
+} from '../components/app/map/MapConstants'
+import { fetchAndStoreTopology, getTopologyAsObject, updateTopologyOnServer } from './objects'
+import { uuid } from 'uuidv4'
+import { addTopology, deleteTopology } from '../api/routes/topologies'
+
+export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = false) {
+ try {
+ const project = yield select((state) => state.objects.project[projectId])
+
+ for (let i in project.topologyIds) {
+ yield fetchAndStoreTopology(project.topologyIds[i])
+ }
+
+ if (setTopology) {
+ yield put(setCurrentTopology(project.topologyIds[0]))
+ }
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddTopology(action) {
+ try {
+ const currentProjectId = yield select((state) => state.currentProjectId)
+
+ let topologyToBeCreated
+ if (action.duplicateId) {
+ topologyToBeCreated = yield getTopologyAsObject(action.duplicateId, false)
+ topologyToBeCreated = Object.assign({}, topologyToBeCreated, {
+ name: action.name,
+ })
+ } else {
+ topologyToBeCreated = { name: action.name, rooms: [] }
+ }
+
+ const topology = yield call(
+ addTopology,
+ Object.assign({}, topologyToBeCreated, {
+ projectId: currentProjectId,
+ })
+ )
+ yield fetchAndStoreTopology(topology._id)
+
+ const topologyIds = yield select((state) => state.objects.project[currentProjectId].topologyIds)
+ yield put(
+ addPropToStoreObject('project', currentProjectId, {
+ topologyIds: topologyIds.concat([topology._id]),
+ })
+ )
+ yield put(setCurrentTopology(topology._id))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteTopology(action) {
+ try {
+ const currentProjectId = yield select((state) => state.currentProjectId)
+ const topologyIds = yield select((state) => state.objects.project[currentProjectId].topologyIds)
+ const currentTopologyId = yield select((state) => state.currentTopologyId)
+ if (currentTopologyId === action.id) {
+ yield put(setCurrentTopology(topologyIds.filter((t) => t !== action.id)[0]))
+ }
+
+ yield call(deleteTopology, action.id)
+
+ yield put(
+ addPropToStoreObject('project', currentProjectId, {
+ topologyIds: topologyIds.filter((id) => id !== action.id),
+ })
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onStartNewRoomConstruction() {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const room = {
+ _id: uuid(),
+ name: 'Room',
+ topologyId,
+ tileIds: [],
+ }
+ yield put(addToStore('room', room))
+ yield put(addIdToStoreObjectListProp('topology', topologyId, 'roomIds', room._id))
+ yield updateTopologyOnServer(topologyId)
+ yield put(startNewRoomConstructionSucceeded(room._id))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onCancelNewRoomConstruction() {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const roomId = yield select((state) => state.construction.currentRoomInConstruction)
+ yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'roomIds', roomId))
+ // TODO remove room from store, too
+ yield updateTopologyOnServer(topologyId)
+ yield put(cancelNewRoomConstructionSucceeded())
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddTile(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const roomId = yield select((state) => state.construction.currentRoomInConstruction)
+ const tile = {
+ _id: uuid(),
+ roomId,
+ positionX: action.positionX,
+ positionY: action.positionY,
+ }
+ yield put(addToStore('tile', tile))
+ yield put(addIdToStoreObjectListProp('room', roomId, 'tileIds', tile._id))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteTile(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const roomId = yield select((state) => state.construction.currentRoomInConstruction)
+ yield put(removeIdFromStoreObjectListProp('room', roomId, 'tileIds', action.tileId))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onEditRoomName(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const roomId = yield select((state) => state.interactionLevel.roomId)
+ const room = Object.assign({}, yield select((state) => state.objects.room[roomId]))
+ room.name = action.name
+ yield put(addPropToStoreObject('room', roomId, { name: action.name }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteRoom() {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const roomId = yield select((state) => state.interactionLevel.roomId)
+ yield put(goDownOneInteractionLevel())
+ yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'roomIds', roomId))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onEditRackName(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId)
+ const rack = Object.assign({}, yield select((state) => state.objects.rack[rackId]))
+ rack.name = action.name
+ yield put(addPropToStoreObject('rack', rackId, { name: action.name }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteRack() {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const tileId = yield select((state) => state.interactionLevel.tileId)
+ yield put(goDownOneInteractionLevel())
+ yield put(addPropToStoreObject('tile', tileId, { rackId: undefined }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddRackToTile(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const rack = {
+ _id: uuid(),
+ name: 'Rack',
+ capacity: DEFAULT_RACK_SLOT_CAPACITY,
+ powerCapacityW: DEFAULT_RACK_POWER_CAPACITY,
+ }
+ rack.machineIds = new Array(rack.capacity).fill(null)
+ yield put(addToStore('rack', rack))
+ yield put(addPropToStoreObject('tile', action.tileId, { rackId: rack._id }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddMachine(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId)
+ const rack = yield select((state) => state.objects.rack[rackId])
+
+ const machine = {
+ _id: uuid(),
+ rackId,
+ position: action.position,
+ cpuIds: [],
+ gpuIds: [],
+ memoryIds: [],
+ storageIds: [],
+ }
+ yield put(addToStore('machine', machine))
+
+ const machineIds = [...rack.machineIds]
+ machineIds[machine.position - 1] = machine._id
+ yield put(addPropToStoreObject('rack', rackId, { machineIds }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteMachine() {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const tileId = yield select((state) => state.interactionLevel.tileId)
+ const position = yield select((state) => state.interactionLevel.position)
+ const rack = yield select((state) => state.objects.rack[state.objects.tile[tileId].rackId])
+ const machineIds = [...rack.machineIds]
+ machineIds[position - 1] = null
+ yield put(goDownOneInteractionLevel())
+ yield put(addPropToStoreObject('rack', rack._id, { machineIds }))
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onAddUnit(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const tileId = yield select((state) => state.interactionLevel.tileId)
+ const position = yield select((state) => state.interactionLevel.position)
+ const machine = yield select(
+ (state) =>
+ state.objects.machine[state.objects.rack[state.objects.tile[tileId].rackId].machineIds[position - 1]]
+ )
+
+ if (machine[action.unitType + 'Ids'].length >= MAX_NUM_UNITS_PER_MACHINE) {
+ return
+ }
+
+ const units = [...machine[action.unitType + 'Ids'], action.id]
+ yield put(
+ addPropToStoreObject('machine', machine._id, {
+ [action.unitType + 'Ids']: units,
+ })
+ )
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onDeleteUnit(action) {
+ try {
+ const topologyId = yield select((state) => state.currentTopologyId)
+ const tileId = yield select((state) => state.interactionLevel.tileId)
+ const position = yield select((state) => state.interactionLevel.position)
+ const machine = yield select(
+ (state) =>
+ state.objects.machine[state.objects.rack[state.objects.tile[tileId].rackId].machineIds[position - 1]]
+ )
+ const unitIds = machine[action.unitType + 'Ids'].slice()
+ unitIds.splice(action.index, 1)
+
+ yield put(
+ addPropToStoreObject('machine', machine._id, {
+ [action.unitType + 'Ids']: unitIds,
+ })
+ )
+ yield updateTopologyOnServer(topologyId)
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/sagas/users.js b/opendc-web/opendc-web-ui/src/sagas/users.js
new file mode 100644
index 00000000..74e652f6
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/sagas/users.js
@@ -0,0 +1,44 @@
+import { call, put } from 'redux-saga/effects'
+import { logInSucceeded } from '../actions/auth'
+import { addToStore } from '../actions/objects'
+import { fetchAuthorizationsOfCurrentUserSucceeded } from '../actions/users'
+import { performTokenSignIn } from '../api/routes/token-signin'
+import { addUser } from '../api/routes/users'
+import { saveAuthLocalStorage } from '../auth/index'
+import { fetchAndStoreProject, fetchAndStoreUser } from './objects'
+
+export function* onFetchLoggedInUser(action) {
+ try {
+ const tokenResponse = yield call(performTokenSignIn, action.payload.authToken)
+
+ let userId = tokenResponse.userId
+
+ if (tokenResponse.isNewUser) {
+ saveAuthLocalStorage({ authToken: action.payload.authToken })
+ const newUser = yield call(addUser, action.payload)
+ userId = newUser._id
+ }
+
+ yield put(logInSucceeded(Object.assign({ userId }, action.payload)))
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export function* onFetchAuthorizationsOfCurrentUser(action) {
+ try {
+ const user = yield call(fetchAndStoreUser, action.userId)
+
+ for (const authorization of user.authorizations) {
+ authorization.userId = action.userId
+ yield put(addToStore('authorization', authorization))
+ yield fetchAndStoreProject(authorization.projectId)
+ }
+
+ const authorizationIds = user.authorizations.map((authorization) => [action.userId, authorization.projectId])
+
+ yield put(fetchAuthorizationsOfCurrentUserSucceeded(authorizationIds))
+ } catch (error) {
+ console.error(error)
+ }
+}