From d9e65dceb38cdb8dc4e464d388755f9456620566 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sun, 16 May 2021 17:07:58 +0200 Subject: ui: Restructure OpenDC frontend This change updates the structure of the OpenDC frontend in order to improve the maintainability of the frontend. --- opendc-web/opendc-web-ui/src/redux/actions/auth.js | 23 ++ .../src/redux/actions/interaction-level.js | 50 ++++ opendc-web/opendc-web-ui/src/redux/actions/map.js | 83 ++++++ .../opendc-web-ui/src/redux/actions/objects.js | 41 +++ .../opendc-web-ui/src/redux/actions/portfolios.js | 41 +++ .../opendc-web-ui/src/redux/actions/prefabs.js | 32 +++ .../opendc-web-ui/src/redux/actions/projects.js | 44 +++ .../opendc-web-ui/src/redux/actions/scenarios.js | 43 +++ .../opendc-web-ui/src/redux/actions/topologies.js | 17 ++ .../src/redux/actions/topology/building.js | 105 +++++++ .../src/redux/actions/topology/machine.js | 25 ++ .../src/redux/actions/topology/rack.js | 23 ++ .../src/redux/actions/topology/room.js | 48 ++++ .../opendc-web-ui/src/redux/actions/users.js | 37 +++ opendc-web/opendc-web-ui/src/redux/index.js | 60 ++++ .../src/redux/middleware/viewport-adjustment.js | 73 +++++ .../opendc-web-ui/src/redux/reducers/auth.js | 12 + .../src/redux/reducers/construction-mode.js | 52 ++++ .../src/redux/reducers/current-ids.js | 54 ++++ .../opendc-web-ui/src/redux/reducers/index.js | 23 ++ .../src/redux/reducers/interaction-level.js | 61 ++++ opendc-web/opendc-web-ui/src/redux/reducers/map.js | 35 +++ .../opendc-web-ui/src/redux/reducers/objects.js | 64 +++++ .../src/redux/reducers/project-list.js | 18 ++ opendc-web/opendc-web-ui/src/redux/sagas/index.js | 80 ++++++ .../opendc-web-ui/src/redux/sagas/objects.js | 229 +++++++++++++++ .../opendc-web-ui/src/redux/sagas/portfolios.js | 131 +++++++++ .../opendc-web-ui/src/redux/sagas/prefabs.js | 15 + .../opendc-web-ui/src/redux/sagas/profile.js | 12 + .../opendc-web-ui/src/redux/sagas/projects.js | 48 ++++ .../opendc-web-ui/src/redux/sagas/scenarios.js | 65 +++++ .../opendc-web-ui/src/redux/sagas/topology.js | 311 +++++++++++++++++++++ opendc-web/opendc-web-ui/src/redux/sagas/users.js | 44 +++ 33 files changed, 1999 insertions(+) create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/auth.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/interaction-level.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/map.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/objects.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/portfolios.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/prefabs.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/projects.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/scenarios.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/topologies.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/topology/building.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/topology/room.js create mode 100644 opendc-web/opendc-web-ui/src/redux/actions/users.js create mode 100644 opendc-web/opendc-web-ui/src/redux/index.js create mode 100644 opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/auth.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/index.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/map.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/objects.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/project-list.js create mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/index.js create mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/objects.js create mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js create mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js create mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/profile.js create mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/projects.js create mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js create mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/topology.js create mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/users.js (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/actions/auth.js b/opendc-web/opendc-web-ui/src/redux/actions/auth.js new file mode 100644 index 00000000..38c1a782 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/auth.js @@ -0,0 +1,23 @@ +export const LOG_IN = 'LOG_IN' +export const LOG_IN_SUCCEEDED = 'LOG_IN_SUCCEEDED' +export const LOG_OUT = 'LOG_OUT' + +export function logIn(payload) { + return { + type: LOG_IN, + payload, + } +} + +export function logInSucceeded(payload) { + return { + type: LOG_IN_SUCCEEDED, + payload, + } +} + +export function logOut() { + return { + type: LOG_OUT, + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/interaction-level.js b/opendc-web/opendc-web-ui/src/redux/actions/interaction-level.js new file mode 100644 index 00000000..ff6b1fa3 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/interaction-level.js @@ -0,0 +1,50 @@ +export const GO_FROM_BUILDING_TO_ROOM = 'GO_FROM_BUILDING_TO_ROOM' +export const GO_FROM_ROOM_TO_RACK = 'GO_FROM_ROOM_TO_RACK' +export const GO_FROM_RACK_TO_MACHINE = 'GO_FROM_RACK_TO_MACHINE' +export const GO_DOWN_ONE_INTERACTION_LEVEL = 'GO_DOWN_ONE_INTERACTION_LEVEL' + +export function goFromBuildingToRoom(roomId) { + return (dispatch, getState) => { + const { interactionLevel } = getState() + if (interactionLevel.mode !== 'BUILDING') { + return + } + + dispatch({ + type: GO_FROM_BUILDING_TO_ROOM, + roomId, + }) + } +} + +export function goFromRoomToRack(tileId) { + return (dispatch, getState) => { + const { interactionLevel } = getState() + if (interactionLevel.mode !== 'ROOM') { + return + } + dispatch({ + type: GO_FROM_ROOM_TO_RACK, + tileId, + }) + } +} + +export function goFromRackToMachine(position) { + return (dispatch, getState) => { + const { interactionLevel } = getState() + if (interactionLevel.mode !== 'RACK') { + return + } + dispatch({ + type: GO_FROM_RACK_TO_MACHINE, + position, + }) + } +} + +export function goDownOneInteractionLevel() { + return { + type: GO_DOWN_ONE_INTERACTION_LEVEL, + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/map.js b/opendc-web/opendc-web-ui/src/redux/actions/map.js new file mode 100644 index 00000000..aa14cacd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/map.js @@ -0,0 +1,83 @@ +import { + MAP_MAX_SCALE, + MAP_MIN_SCALE, + MAP_SCALE_PER_EVENT, + MAP_SIZE_IN_PIXELS, +} from '../../components/app/map/MapConstants' + +export const SET_MAP_POSITION = 'SET_MAP_POSITION' +export const SET_MAP_DIMENSIONS = 'SET_MAP_DIMENSIONS' +export const SET_MAP_SCALE = 'SET_MAP_SCALE' + +export function setMapPosition(x, y) { + return { + type: SET_MAP_POSITION, + x, + y, + } +} + +export function setMapDimensions(width, height) { + return { + type: SET_MAP_DIMENSIONS, + width, + height, + } +} + +export function setMapScale(scale) { + return { + type: SET_MAP_SCALE, + scale, + } +} + +export function zoomInOnCenter(zoomIn) { + return (dispatch, getState) => { + const state = getState() + + dispatch(zoomInOnPosition(zoomIn, state.map.dimensions.width / 2, state.map.dimensions.height / 2)) + } +} + +export function zoomInOnPosition(zoomIn, x, y) { + return (dispatch, getState) => { + const state = getState() + + const centerPoint = { + x: x / state.map.scale - state.map.position.x / state.map.scale, + y: y / state.map.scale - state.map.position.y / state.map.scale, + } + const newScale = zoomIn ? state.map.scale * MAP_SCALE_PER_EVENT : state.map.scale / MAP_SCALE_PER_EVENT + const boundedScale = Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE) + + const newX = -(centerPoint.x - x / boundedScale) * boundedScale + const newY = -(centerPoint.y - y / boundedScale) * boundedScale + + dispatch(setMapPositionWithBoundsCheck(newX, newY)) + dispatch(setMapScale(boundedScale)) + } +} + +export function setMapPositionWithBoundsCheck(x, y) { + return (dispatch, getState) => { + const state = getState() + + const scaledMapSize = MAP_SIZE_IN_PIXELS * state.map.scale + + const updatedX = + x > 0 + ? 0 + : x < -scaledMapSize + state.map.dimensions.width + ? -scaledMapSize + state.map.dimensions.width + : x + const updatedY = + y > 0 + ? 0 + : y < -scaledMapSize + state.map.dimensions.height + ? -scaledMapSize + state.map.dimensions.height + : y + + dispatch(setMapPosition(updatedX, updatedY)) + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/objects.js b/opendc-web/opendc-web-ui/src/redux/actions/objects.js new file mode 100644 index 00000000..7b648b18 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/objects.js @@ -0,0 +1,41 @@ +export const ADD_TO_STORE = 'ADD_TO_STORE' +export const ADD_PROP_TO_STORE_OBJECT = 'ADD_PROP_TO_STORE_OBJECT' +export const ADD_ID_TO_STORE_OBJECT_LIST_PROP = 'ADD_ID_TO_STORE_OBJECT_LIST_PROP' +export const REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP = 'REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP' + +export function addToStore(objectType, object) { + return { + type: ADD_TO_STORE, + objectType, + object, + } +} + +export function addPropToStoreObject(objectType, objectId, propObject) { + return { + type: ADD_PROP_TO_STORE_OBJECT, + objectType, + objectId, + propObject, + } +} + +export function addIdToStoreObjectListProp(objectType, objectId, propName, id) { + return { + type: ADD_ID_TO_STORE_OBJECT_LIST_PROP, + objectType, + objectId, + propName, + id, + } +} + +export function removeIdFromStoreObjectListProp(objectType, objectId, propName, id) { + return { + type: REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP, + objectType, + objectId, + propName, + id, + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js b/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js new file mode 100644 index 00000000..d37886d8 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js @@ -0,0 +1,41 @@ +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/prefabs.js b/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js new file mode 100644 index 00000000..c112feed --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js @@ -0,0 +1,32 @@ +export const ADD_PREFAB = 'ADD_PREFAB' +export const DELETE_PREFAB = 'DELETE_PREFAB' +export const DELETE_PREFAB_SUCCEEDED = 'DELETE_PREFAB_SUCCEEDED' +export const OPEN_PREFAB_SUCCEEDED = 'OPEN_PREFAB_SUCCEEDED' + +export function addPrefab(name) { + return { + type: ADD_PREFAB, + name, + } +} + +export function deletePrefab(id) { + return { + type: DELETE_PREFAB, + id, + } +} + +export function deletePrefabSucceeded(id) { + return { + type: DELETE_PREFAB_SUCCEEDED, + id, + } +} + +export function openPrefabSucceeded(id) { + return { + type: OPEN_PREFAB_SUCCEEDED, + id, + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/projects.js b/opendc-web/opendc-web-ui/src/redux/actions/projects.js new file mode 100644 index 00000000..15158164 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/projects.js @@ -0,0 +1,44 @@ +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 addProject(name) { + return (dispatch, getState) => { + const { auth } = getState() + dispatch({ + type: ADD_PROJECT, + name, + userId: auth.userId, + }) + } +} + +export function addProjectSucceeded(authorization) { + return { + type: ADD_PROJECT_SUCCEEDED, + authorization, + } +} + +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, + id, + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js b/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js new file mode 100644 index 00000000..c8a90762 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js @@ -0,0 +1,43 @@ +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 new file mode 100644 index 00000000..dcce3b7d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/topologies.js @@ -0,0 +1,17 @@ +export const ADD_TOPOLOGY = 'ADD_TOPOLOGY' +export const DELETE_TOPOLOGY = 'DELETE_TOPOLOGY' + +export function addTopology(name, duplicateId) { + return { + type: ADD_TOPOLOGY, + name, + duplicateId, + } +} + +export function deleteTopology(id) { + return { + type: DELETE_TOPOLOGY, + id, + } +} 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 new file mode 100644 index 00000000..72deda6f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js @@ -0,0 +1,105 @@ +export const SET_CURRENT_TOPOLOGY = 'SET_CURRENT_TOPOLOGY' +export const START_NEW_ROOM_CONSTRUCTION = 'START_NEW_ROOM_CONSTRUCTION' +export const START_NEW_ROOM_CONSTRUCTION_SUCCEEDED = 'START_NEW_ROOM_CONSTRUCTION_SUCCEEDED' +export const FINISH_NEW_ROOM_CONSTRUCTION = 'FINISH_NEW_ROOM_CONSTRUCTION' +export const CANCEL_NEW_ROOM_CONSTRUCTION = 'CANCEL_NEW_ROOM_CONSTRUCTION' +export const CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED = 'CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED' +export const START_ROOM_EDIT = 'START_ROOM_EDIT' +export const FINISH_ROOM_EDIT = 'FINISH_ROOM_EDIT' +export const ADD_TILE = 'ADD_TILE' +export const DELETE_TILE = 'DELETE_TILE' + +export function setCurrentTopology(topologyId) { + return { + type: SET_CURRENT_TOPOLOGY, + topologyId, + } +} + +export function startNewRoomConstruction() { + return { + type: START_NEW_ROOM_CONSTRUCTION, + } +} + +export function startNewRoomConstructionSucceeded(roomId) { + return { + type: START_NEW_ROOM_CONSTRUCTION_SUCCEEDED, + roomId, + } +} + +export function finishNewRoomConstruction() { + return (dispatch, getState) => { + const { objects, construction } = getState() + if (objects.room[construction.currentRoomInConstruction].tileIds.length === 0) { + dispatch(cancelNewRoomConstruction()) + return + } + + dispatch({ + type: FINISH_NEW_ROOM_CONSTRUCTION, + }) + } +} + +export function cancelNewRoomConstruction() { + return { + type: CANCEL_NEW_ROOM_CONSTRUCTION, + } +} + +export function cancelNewRoomConstructionSucceeded() { + return { + type: CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED, + } +} + +export function startRoomEdit() { + return (dispatch, getState) => { + const { interactionLevel } = getState() + dispatch({ + type: START_ROOM_EDIT, + roomId: interactionLevel.roomId, + }) + } +} + +export function finishRoomEdit() { + return { + type: FINISH_ROOM_EDIT, + } +} + +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])) + return + } + } + dispatch(addTile(positionX, positionY)) + } +} + +export function addTile(positionX, positionY) { + return { + type: ADD_TILE, + positionX, + positionY, + } +} + +export function deleteTile(tileId) { + return { + type: DELETE_TILE, + tileId, + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js new file mode 100644 index 00000000..17ccce5d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js @@ -0,0 +1,25 @@ +export const DELETE_MACHINE = 'DELETE_MACHINE' +export const ADD_UNIT = 'ADD_UNIT' +export const DELETE_UNIT = 'DELETE_UNIT' + +export function deleteMachine() { + return { + type: DELETE_MACHINE, + } +} + +export function addUnit(unitType, id) { + return { + type: ADD_UNIT, + unitType, + id, + } +} + +export function deleteUnit(unitType, index) { + return { + type: DELETE_UNIT, + unitType, + index, + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js new file mode 100644 index 00000000..b117402e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js @@ -0,0 +1,23 @@ +export const EDIT_RACK_NAME = 'EDIT_RACK_NAME' +export const DELETE_RACK = 'DELETE_RACK' +export const ADD_MACHINE = 'ADD_MACHINE' + +export function editRackName(name) { + return { + type: EDIT_RACK_NAME, + name, + } +} + +export function deleteRack() { + return { + type: DELETE_RACK, + } +} + +export function addMachine(position) { + return { + type: ADD_MACHINE, + position, + } +} 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 new file mode 100644 index 00000000..61eea7fe --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js @@ -0,0 +1,48 @@ +import { findTileWithPosition } from '../../../util/tile-calculations' + +export const EDIT_ROOM_NAME = 'EDIT_ROOM_NAME' +export const DELETE_ROOM = 'DELETE_ROOM' +export const START_RACK_CONSTRUCTION = 'START_RACK_CONSTRUCTION' +export const STOP_RACK_CONSTRUCTION = 'STOP_RACK_CONSTRUCTION' +export const ADD_RACK_TO_TILE = 'ADD_RACK_TO_TILE' + +export function editRoomName(name) { + return { + type: EDIT_ROOM_NAME, + name, + } +} + +export function startRackConstruction() { + return { + type: START_RACK_CONSTRUCTION, + } +} + +export function stopRackConstruction() { + return { + type: STOP_RACK_CONSTRUCTION, + } +} + +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 tile = findTileWithPosition(tiles, positionX, positionY) + + if (tile !== null) { + dispatch({ + type: ADD_RACK_TO_TILE, + tileId: tile._id, + }) + } + } +} + +export function deleteRoom() { + return { + type: DELETE_ROOM, + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/users.js b/opendc-web/opendc-web-ui/src/redux/actions/users.js new file mode 100644 index 00000000..4868ac34 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/actions/users.js @@ -0,0 +1,37 @@ +export const FETCH_AUTHORIZATIONS_OF_CURRENT_USER = 'FETCH_AUTHORIZATIONS_OF_CURRENT_USER' +export const FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED = 'FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED' +export const DELETE_CURRENT_USER = 'DELETE_CURRENT_USER' +export const DELETE_CURRENT_USER_SUCCEEDED = 'DELETE_CURRENT_USER_SUCCEEDED' + +export function fetchAuthorizationsOfCurrentUser() { + return (dispatch, getState) => { + const { auth } = getState() + dispatch({ + type: FETCH_AUTHORIZATIONS_OF_CURRENT_USER, + userId: auth.userId, + }) + } +} + +export function fetchAuthorizationsOfCurrentUserSucceeded(authorizationsOfCurrentUser) { + return { + type: FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED, + authorizationsOfCurrentUser, + } +} + +export function deleteCurrentUser() { + return (dispatch, getState) => { + const { auth } = getState() + dispatch({ + type: DELETE_CURRENT_USER, + userId: auth.userId, + }) + } +} + +export function deleteCurrentUserSucceeded() { + return { + type: DELETE_CURRENT_USER_SUCCEEDED, + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/index.js b/opendc-web/opendc-web-ui/src/redux/index.js new file mode 100644 index 00000000..c706752b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/index.js @@ -0,0 +1,60 @@ +import { useMemo } from 'react' +import { applyMiddleware, compose, createStore } from 'redux' +import { createLogger } from 'redux-logger' +import persistState from 'redux-localstorage' +import createSagaMiddleware from 'redux-saga' +import thunk from 'redux-thunk' +import { authRedirectMiddleware } from '../auth' +import rootReducer from './reducers' +import rootSaga from './sagas' +import { viewportAdjustmentMiddleware } from './middleware/viewport-adjustment' + +let store + +function initStore(initialState) { + const sagaMiddleware = createSagaMiddleware() + + const middlewares = [thunk, sagaMiddleware, authRedirectMiddleware, viewportAdjustmentMiddleware] + + if (process.env.NODE_ENV !== 'production') { + middlewares.push(createLogger()) + } + + let enhancer = applyMiddleware(...middlewares) + + if (global.localStorage) { + enhancer = compose(persistState('auth'), enhancer) + } + + const configuredStore = createStore(rootReducer, enhancer) + sagaMiddleware.run(rootSaga) + store = configuredStore + + return configuredStore +} + +export const initializeStore = (preloadedState) => { + let _store = store ?? initStore(preloadedState) + + // After navigating to a page with an initial Redux state, merge that state + // with the current state in the store, and create a new store + if (preloadedState && store) { + _store = initStore({ + ...store.getState(), + ...preloadedState, + }) + // Reset the current store + store = undefined + } + + // For SSG and SSR always create a new store + if (typeof window === 'undefined') return _store + // Create the store once in the client + if (!store) store = _store + + return _store +} + +export function useStore(initialState) { + return useMemo(() => initializeStore(initialState), [initialState]) +} 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 new file mode 100644 index 00000000..6b22eb80 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js @@ -0,0 +1,73 @@ +import { SET_MAP_DIMENSIONS, setMapPosition, setMapScale } from '../actions/map' +import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building' +import { + MAP_MAX_SCALE, + MAP_MIN_SCALE, + SIDEBAR_WIDTH, + TILE_SIZE_IN_PIXELS, + VIEWPORT_PADDING, +} from '../../components/app/map/MapConstants' +import { calculateRoomListBounds } from '../../util/tile-calculations' + +export const viewportAdjustmentMiddleware = (store) => (next) => (action) => { + const state = store.getState() + + let topologyId = '-1' + let mapDimensions = {} + if (action.type === SET_CURRENT_TOPOLOGY && action.topologyId !== '-1') { + topologyId = action.topologyId + mapDimensions = state.map.dimensions + } else if (action.type === SET_MAP_DIMENSIONS && state.currentTopologyId !== '-1') { + topologyId = state.currentTopologyId + mapDimensions = { width: action.width, height: action.height } + } + + if (topologyId !== '-1') { + const roomIds = state.objects.topology[topologyId].roomIds + const rooms = roomIds.map((id) => Object.assign({}, state.objects.room[id])) + rooms.forEach((room) => (room.tiles = room.tileIds.map((tileId) => state.objects.tile[tileId]))) + + let hasNoTiles = true + for (let i in rooms) { + if (rooms[i].tiles.length > 0) { + hasNoTiles = false + break + } + } + + if (!hasNoTiles) { + const viewportParams = calculateParametersToZoomInOnRooms(rooms, mapDimensions.width, mapDimensions.height) + store.dispatch(setMapPosition(viewportParams.newX, viewportParams.newY)) + store.dispatch(setMapScale(viewportParams.newScale)) + } + } + + next(action) +} + +function calculateParametersToZoomInOnRooms(rooms, mapWidth, mapHeight) { + const bounds = calculateRoomListBounds(rooms) + const newScale = calculateNewScale(bounds, mapWidth, mapHeight) + + // Coordinates of the center of the room, relative to the global origin of the map + const roomCenterCoordinates = { + x: bounds.center.x * TILE_SIZE_IN_PIXELS * newScale, + y: bounds.center.y * TILE_SIZE_IN_PIXELS * newScale, + } + + const newX = -roomCenterCoordinates.x + mapWidth / 2 + const newY = -roomCenterCoordinates.y + mapHeight / 2 + + return { newScale, newX, newY } +} + +function calculateNewScale(bounds, mapWidth, mapHeight) { + const width = bounds.max.x - bounds.min.x + const height = bounds.max.y - bounds.min.y + + const scaleX = (mapWidth - 2 * SIDEBAR_WIDTH) / (width * TILE_SIZE_IN_PIXELS + 2 * VIEWPORT_PADDING) + const scaleY = mapHeight / (height * TILE_SIZE_IN_PIXELS + 2 * VIEWPORT_PADDING) + const newScale = Math.min(scaleX, scaleY) + + return Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE) +} diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/auth.js b/opendc-web/opendc-web-ui/src/redux/reducers/auth.js new file mode 100644 index 00000000..399a4b10 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/auth.js @@ -0,0 +1,12 @@ +import { LOG_IN_SUCCEEDED, LOG_OUT } from '../actions/auth' + +export function auth(state = {}, action) { + switch (action.type) { + case LOG_IN_SUCCEEDED: + return action.payload + case LOG_OUT: + return {} + default: + return state + } +} 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 new file mode 100644 index 00000000..257dddd2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js @@ -0,0 +1,52 @@ +import { combineReducers } from 'redux' +import { GO_DOWN_ONE_INTERACTION_LEVEL } from '../actions/interaction-level' +import { + CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED, + FINISH_NEW_ROOM_CONSTRUCTION, + FINISH_ROOM_EDIT, + SET_CURRENT_TOPOLOGY, + START_NEW_ROOM_CONSTRUCTION_SUCCEEDED, + 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) { + case START_NEW_ROOM_CONSTRUCTION_SUCCEEDED: + return action.roomId + case START_ROOM_EDIT: + 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: + return '-1' + default: + return state + } +} + +export function inRackConstructionMode(state = false, action) { + switch (action.type) { + 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 + default: + return state + } +} + +export const construction = combineReducers({ + currentRoomInConstruction, + inRackConstructionMode, +}) 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 new file mode 100644 index 00000000..9b46aa60 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js @@ -0,0 +1,54 @@ +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) { + case SET_CURRENT_TOPOLOGY: + return action.topologyId + default: + 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 new file mode 100644 index 00000000..9dff379b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/index.js @@ -0,0 +1,23 @@ +import { combineReducers } from 'redux' +import { auth } from './auth' +import { construction } from './construction-mode' +import { currentPortfolioId, currentProjectId, currentScenarioId, currentTopologyId } from './current-ids' +import { interactionLevel } from './interaction-level' +import { map } from './map' +import { objects } from './objects' +import { projectList } from './project-list' + +const rootReducer = combineReducers({ + objects, + projectList, + construction, + map, + currentProjectId, + currentTopologyId, + currentPortfolioId, + currentScenarioId, + interactionLevel, + auth, +}) + +export default rootReducer 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 new file mode 100644 index 00000000..eafcb269 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js @@ -0,0 +1,61 @@ +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', + } + case GO_FROM_BUILDING_TO_ROOM: + return { + mode: 'ROOM', + roomId: action.roomId, + } + case GO_FROM_ROOM_TO_RACK: + return { + mode: 'RACK', + roomId: state.roomId, + tileId: action.tileId, + } + case GO_FROM_RACK_TO_MACHINE: + return { + mode: 'MACHINE', + roomId: state.roomId, + tileId: state.tileId, + position: action.position, + } + case GO_DOWN_ONE_INTERACTION_LEVEL: + if (state.mode === 'ROOM') { + return { + mode: 'BUILDING', + } + } else if (state.mode === 'RACK') { + return { + mode: 'ROOM', + roomId: state.roomId, + } + } else if (state.mode === 'MACHINE') { + return { + mode: 'RACK', + roomId: state.roomId, + tileId: state.tileId, + } + } else { + return state + } + default: + return state + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/map.js b/opendc-web/opendc-web-ui/src/redux/reducers/map.js new file mode 100644 index 00000000..de712c15 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/map.js @@ -0,0 +1,35 @@ +import { combineReducers } from 'redux' +import { SET_MAP_DIMENSIONS, SET_MAP_POSITION, SET_MAP_SCALE } from '../actions/map' + +export function position(state = { x: 0, y: 0 }, action) { + switch (action.type) { + case SET_MAP_POSITION: + return { x: action.x, y: action.y } + default: + return state + } +} + +export function dimensions(state = { width: 600, height: 400 }, action) { + switch (action.type) { + case SET_MAP_DIMENSIONS: + return { width: action.width, height: action.height } + default: + return state + } +} + +export function scale(state = 1, action) { + switch (action.type) { + case SET_MAP_SCALE: + return action.scale + default: + return state + } +} + +export const map = combineReducers({ + position, + dimensions, + scale, +}) diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/objects.js b/opendc-web/opendc-web-ui/src/redux/reducers/objects.js new file mode 100644 index 00000000..a2483b43 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/objects.js @@ -0,0 +1,64 @@ +import { combineReducers } from 'redux' +import { + ADD_ID_TO_STORE_OBJECT_LIST_PROP, + ADD_PROP_TO_STORE_OBJECT, + ADD_TO_STORE, + REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP, +} from '../actions/objects' +import { CPU_UNITS, GPU_UNITS, MEMORY_UNITS, STORAGE_UNITS } from '../../util/unit-specifications' + +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), + storage: object('storage', STORAGE_UNITS), + machine: object('machine'), + rack: object('rack'), + tile: object('tile'), + room: object('room'), + topology: object('topology'), + trace: object('trace'), + scheduler: object('scheduler'), + portfolio: object('portfolio'), + scenario: object('scenario'), + prefab: object('prefab'), +}) + +function object(type, defaultState = {}) { + return objectWithId(type, (object) => object._id, defaultState) +} + +function objectWithId(type, getId, defaultState = {}) { + return (state = defaultState, action) => { + if (action.objectType !== type) { + return state + } + + if (action.type === ADD_TO_STORE) { + return Object.assign({}, 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), + }) + } else if (action.type === ADD_ID_TO_STORE_OBJECT_LIST_PROP) { + return Object.assign({}, state, { + [action.objectId]: Object.assign({}, state[action.objectId], { + [action.propName]: [...state[action.objectId][action.propName], action.id], + }), + }) + } else if (action.type === REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP) { + return Object.assign({}, state, { + [action.objectId]: Object.assign({}, state[action.objectId], { + [action.propName]: state[action.objectId][action.propName].filter((id) => id !== action.id), + }), + }) + } + + return state + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/project-list.js b/opendc-web/opendc-web-ui/src/redux/reducers/project-list.js new file mode 100644 index 00000000..ad803db0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/project-list.js @@ -0,0 +1,18 @@ +import { combineReducers } from 'redux' +import { ADD_PROJECT_SUCCEEDED, DELETE_PROJECT_SUCCEEDED } from '../actions/projects' +import { FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED } from '../actions/users' + +export function authorizationsOfCurrentUser(state = [], action) { + switch (action.type) { + case FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED: + return action.authorizationsOfCurrentUser + case ADD_PROJECT_SUCCEEDED: + return [...state, action.authorization] + case DELETE_PROJECT_SUCCEEDED: + return state.filter((authorization) => authorization[1] !== action.id) + default: + return state + } +} + +export const projectList = combineReducers({ authorizationsOfCurrentUser }) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/index.js b/opendc-web/opendc-web-ui/src/redux/sagas/index.js new file mode 100644 index 00000000..6332b2fb --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/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/redux/sagas/objects.js b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js new file mode 100644 index 00000000..82dbb935 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js @@ -0,0 +1,229 @@ +import { call, put, select } 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 { getUser } from '../../api/users' +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 = (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/redux/sagas/portfolios.js b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js new file mode 100644 index 00000000..8ddf888d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/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/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 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/redux/sagas/prefabs.js b/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js new file mode 100644 index 00000000..3158a219 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js @@ -0,0 +1,15 @@ +import { call, put, select } from 'redux-saga/effects' +import { addToStore } from '../actions/objects' +import { addPrefab } from '../../api/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/redux/sagas/profile.js b/opendc-web/opendc-web-ui/src/redux/sagas/profile.js new file mode 100644 index 00000000..e187b765 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/sagas/profile.js @@ -0,0 +1,12 @@ +import { call, put } from 'redux-saga/effects' +import { deleteCurrentUserSucceeded } from '../actions/users' +import { deleteUser } from '../../api/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/redux/sagas/projects.js b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js new file mode 100644 index 00000000..ecd9a7c9 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/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/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/redux/sagas/scenarios.js b/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js new file mode 100644 index 00000000..a5463fa6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/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/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 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/redux/sagas/topology.js b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js new file mode 100644 index 00000000..65f97cc9 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/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/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/redux/sagas/users.js b/opendc-web/opendc-web-ui/src/redux/sagas/users.js new file mode 100644 index 00000000..88c424b5 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/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/token-signin' +import { addUser } from '../../api/users' +import { saveAuthLocalStorage } from '../../auth' +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) + } +} -- cgit v1.2.3 From a6865b86cc8d710374fc0b6cfcbd2b863f1942a9 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sun, 16 May 2021 23:18:02 +0200 Subject: ui: Migrate to Auth0 as Identity Provider This change updates the frontend codebase to move away from the Google login and instead use Auth0 as generic Identity Provider. This allows users to login with other accounts as well. Since Auth0 has a free tier, users can experiment themselves with OpenDC locally without having to pay for the login functionality. The code has been written so that we should be able to migrate away from Auth0 once it is not a suitable Identity Provider for OpenDC anymore. --- opendc-web/opendc-web-ui/src/redux/actions/auth.js | 23 ----------- .../opendc-web-ui/src/redux/actions/projects.js | 29 +++++++++----- .../opendc-web-ui/src/redux/actions/users.js | 37 ------------------ opendc-web/opendc-web-ui/src/redux/index.js | 26 +++++-------- .../opendc-web-ui/src/redux/reducers/auth.js | 12 ------ .../opendc-web-ui/src/redux/reducers/index.js | 6 +-- .../src/redux/reducers/project-list.js | 18 --------- .../opendc-web-ui/src/redux/reducers/projects.js | 14 +++++++ opendc-web/opendc-web-ui/src/redux/sagas/index.js | 14 ++----- .../opendc-web-ui/src/redux/sagas/objects.js | 25 +++++++----- .../opendc-web-ui/src/redux/sagas/portfolios.js | 19 ++++++---- .../opendc-web-ui/src/redux/sagas/prefabs.js | 5 ++- .../opendc-web-ui/src/redux/sagas/profile.js | 12 ------ .../opendc-web-ui/src/redux/sagas/projects.js | 35 +++++++++-------- .../opendc-web-ui/src/redux/sagas/scenarios.js | 14 ++++--- .../opendc-web-ui/src/redux/sagas/topology.js | 7 +++- opendc-web/opendc-web-ui/src/redux/sagas/users.js | 44 ---------------------- 17 files changed, 112 insertions(+), 228 deletions(-) delete mode 100644 opendc-web/opendc-web-ui/src/redux/actions/auth.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/actions/users.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/auth.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/project-list.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/projects.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/profile.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/users.js (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/actions/auth.js b/opendc-web/opendc-web-ui/src/redux/actions/auth.js deleted file mode 100644 index 38c1a782..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/auth.js +++ /dev/null @@ -1,23 +0,0 @@ -export const LOG_IN = 'LOG_IN' -export const LOG_IN_SUCCEEDED = 'LOG_IN_SUCCEEDED' -export const LOG_OUT = 'LOG_OUT' - -export function logIn(payload) { - return { - type: LOG_IN, - payload, - } -} - -export function logInSucceeded(payload) { - return { - type: LOG_IN_SUCCEEDED, - payload, - } -} - -export function logOut() { - return { - type: LOG_OUT, - } -} 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 15158164..a6324c43 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/projects.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/projects.js @@ -1,24 +1,35 @@ +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 (dispatch, getState) => { - const { auth } = getState() - dispatch({ - type: ADD_PROJECT, - name, - userId: auth.userId, - }) + return { + type: ADD_PROJECT, + name, } } -export function addProjectSucceeded(authorization) { +export function addProjectSucceeded(project) { return { type: ADD_PROJECT_SUCCEEDED, - authorization, + project, } } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/users.js b/opendc-web/opendc-web-ui/src/redux/actions/users.js deleted file mode 100644 index 4868ac34..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/users.js +++ /dev/null @@ -1,37 +0,0 @@ -export const FETCH_AUTHORIZATIONS_OF_CURRENT_USER = 'FETCH_AUTHORIZATIONS_OF_CURRENT_USER' -export const FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED = 'FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED' -export const DELETE_CURRENT_USER = 'DELETE_CURRENT_USER' -export const DELETE_CURRENT_USER_SUCCEEDED = 'DELETE_CURRENT_USER_SUCCEEDED' - -export function fetchAuthorizationsOfCurrentUser() { - return (dispatch, getState) => { - const { auth } = getState() - dispatch({ - type: FETCH_AUTHORIZATIONS_OF_CURRENT_USER, - userId: auth.userId, - }) - } -} - -export function fetchAuthorizationsOfCurrentUserSucceeded(authorizationsOfCurrentUser) { - return { - type: FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED, - authorizationsOfCurrentUser, - } -} - -export function deleteCurrentUser() { - return (dispatch, getState) => { - const { auth } = getState() - dispatch({ - type: DELETE_CURRENT_USER, - userId: auth.userId, - }) - } -} - -export function deleteCurrentUserSucceeded() { - return { - type: DELETE_CURRENT_USER_SUCCEEDED, - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/index.js b/opendc-web/opendc-web-ui/src/redux/index.js index c706752b..ee6ca3f5 100644 --- a/opendc-web/opendc-web-ui/src/redux/index.js +++ b/opendc-web/opendc-web-ui/src/redux/index.js @@ -1,40 +1,32 @@ import { useMemo } from 'react' -import { applyMiddleware, compose, createStore } from 'redux' +import { applyMiddleware, createStore } from 'redux' import { createLogger } from 'redux-logger' -import persistState from 'redux-localstorage' import createSagaMiddleware from 'redux-saga' import thunk from 'redux-thunk' -import { authRedirectMiddleware } from '../auth' import rootReducer from './reducers' import rootSaga from './sagas' import { viewportAdjustmentMiddleware } from './middleware/viewport-adjustment' let store -function initStore(initialState) { - const sagaMiddleware = createSagaMiddleware() +function initStore(initialState, ctx) { + const sagaMiddleware = createSagaMiddleware({ context: ctx }) - const middlewares = [thunk, sagaMiddleware, authRedirectMiddleware, viewportAdjustmentMiddleware] + const middlewares = [thunk, sagaMiddleware, viewportAdjustmentMiddleware] if (process.env.NODE_ENV !== 'production') { middlewares.push(createLogger()) } - let enhancer = applyMiddleware(...middlewares) - - if (global.localStorage) { - enhancer = compose(persistState('auth'), enhancer) - } - - const configuredStore = createStore(rootReducer, enhancer) + const configuredStore = createStore(rootReducer, initialState, applyMiddleware(...middlewares)) sagaMiddleware.run(rootSaga) store = configuredStore return configuredStore } -export const initializeStore = (preloadedState) => { - let _store = store ?? initStore(preloadedState) +export const initializeStore = (preloadedState, ctx) => { + let _store = store ?? initStore(preloadedState, ctx) // After navigating to a page with an initial Redux state, merge that state // with the current state in the store, and create a new store @@ -55,6 +47,6 @@ export const initializeStore = (preloadedState) => { return _store } -export function useStore(initialState) { - return useMemo(() => initializeStore(initialState), [initialState]) +export function useStore(initialState, ctx) { + return useMemo(() => initializeStore(initialState, ctx), [initialState, ctx]) } diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/auth.js b/opendc-web/opendc-web-ui/src/redux/reducers/auth.js deleted file mode 100644 index 399a4b10..00000000 --- a/opendc-web/opendc-web-ui/src/redux/reducers/auth.js +++ /dev/null @@ -1,12 +0,0 @@ -import { LOG_IN_SUCCEEDED, LOG_OUT } from '../actions/auth' - -export function auth(state = {}, action) { - switch (action.type) { - case LOG_IN_SUCCEEDED: - return action.payload - case LOG_OUT: - return {} - 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 9dff379b..b143d417 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/index.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/index.js @@ -1,15 +1,14 @@ import { combineReducers } from 'redux' -import { auth } from './auth' import { construction } from './construction-mode' import { currentPortfolioId, currentProjectId, currentScenarioId, currentTopologyId } from './current-ids' import { interactionLevel } from './interaction-level' import { map } from './map' import { objects } from './objects' -import { projectList } from './project-list' +import { projects } from './projects' const rootReducer = combineReducers({ objects, - projectList, + projects, construction, map, currentProjectId, @@ -17,7 +16,6 @@ const rootReducer = combineReducers({ currentPortfolioId, currentScenarioId, interactionLevel, - auth, }) export default rootReducer diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/project-list.js b/opendc-web/opendc-web-ui/src/redux/reducers/project-list.js deleted file mode 100644 index ad803db0..00000000 --- a/opendc-web/opendc-web-ui/src/redux/reducers/project-list.js +++ /dev/null @@ -1,18 +0,0 @@ -import { combineReducers } from 'redux' -import { ADD_PROJECT_SUCCEEDED, DELETE_PROJECT_SUCCEEDED } from '../actions/projects' -import { FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED } from '../actions/users' - -export function authorizationsOfCurrentUser(state = [], action) { - switch (action.type) { - case FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED: - return action.authorizationsOfCurrentUser - case ADD_PROJECT_SUCCEEDED: - return [...state, action.authorization] - case DELETE_PROJECT_SUCCEEDED: - return state.filter((authorization) => authorization[1] !== action.id) - default: - return state - } -} - -export const projectList = combineReducers({ authorizationsOfCurrentUser }) diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/projects.js b/opendc-web/opendc-web-ui/src/redux/reducers/projects.js new file mode 100644 index 00000000..a920e47f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/projects.js @@ -0,0 +1,14 @@ +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 6332b2fb..a8f44843 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/index.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/index.js @@ -1,7 +1,6 @@ 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_PROJECT, DELETE_PROJECT, FETCH_PROJECTS, OPEN_PROJECT_SUCCEEDED } from '../actions/projects' import { ADD_TILE, CANCEL_NEW_ROOM_CONSTRUCTION, @@ -11,10 +10,8 @@ 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 { 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 { onFetchProjects, onOpenProjectSucceeded, onProjectAdd, onProjectDelete } from './projects' import { onAddMachine, onAddRackToTile, @@ -32,7 +29,6 @@ import { 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' @@ -40,14 +36,10 @@ 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(FETCH_PROJECTS, onFetchProjects) 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) 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 82dbb935..e5fd092d 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js @@ -1,9 +1,8 @@ -import { call, put, select } from 'redux-saga/effects' +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 { getUser } from '../../api/users' import { getTopology, updateTopology } from '../../api/topologies' import { uuid } from 'uuidv4' @@ -42,9 +41,10 @@ function* fetchAndStoreObjects(objectType, apiCall) { return objects } -export const fetchAndStoreProject = (id) => fetchAndStoreObject('project', id, call(getProject, id)) - -export const fetchAndStoreUser = (id) => fetchAndStoreObject('user', id, call(getUser, id)) +export const fetchAndStoreProject = function* (id) { + const auth = yield getContext('auth') + return yield fetchAndStoreObject('project', id, call(getProject, auth, id)) +} export const fetchAndStoreTopology = function* (id) { const topologyStore = yield select(OBJECT_SELECTORS['topology']) @@ -52,10 +52,11 @@ export const fetchAndStoreTopology = function* (id) { 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] if (!topology) { - const fullTopology = yield call(getTopology, id) + const fullTopology = yield call(getTopology, auth, id) for (let roomIdx in fullTopology.rooms) { const fullRoom = fullTopology.rooms[roomIdx] @@ -142,8 +143,8 @@ const generateIdIfNotPresent = (obj) => { export const updateTopologyOnServer = function* (id) { const topology = yield getTopologyAsObject(id, true) - - yield call(updateTopology, topology) + const auth = yield getContext('auth') + yield call(updateTopology, auth, topology) } export const getTopologyAsObject = function* (id, keepIds) { @@ -217,10 +218,14 @@ export const getRackById = function* (id, keepIds) { } } -export const fetchAndStoreAllTraces = () => fetchAndStoreObjects('trace', call(getAllTraces)) +export const fetchAndStoreAllTraces = function* () { + const auth = yield getContext('auth') + return yield fetchAndStoreObjects('trace', call(getAllTraces, auth)) +} export const fetchAndStoreAllSchedulers = function* () { - const objects = yield call(getAllSchedulers) + 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)) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js index 8ddf888d..340cb490 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js @@ -1,4 +1,4 @@ -import { call, put, select, delay } from 'redux-saga/effects' +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' @@ -8,7 +8,8 @@ import { getScenario } from '../../api/scenarios' export function* onOpenPortfolioSucceeded(action) { try { - const project = yield call(getProject, action.projectId) + const auth = yield getContext('auth') + const project = yield call(getProject, auth, action.projectId) yield put(addToStore('project', project)) yield fetchAndStoreAllTopologiesOfProject(project._id) yield fetchPortfoliosOfProject() @@ -66,11 +67,12 @@ export function* fetchPortfoliosOfProject() { export function* fetchPortfolioWithScenarios(portfolioId) { try { - const portfolio = yield call(getPortfolio, portfolioId) + 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, portfolio.scenarioIds[i]) + const scenario = yield call(getScenario, auth, portfolio.scenarioIds[i]) yield put(addToStore('scenario', scenario)) } return portfolio @@ -82,9 +84,10 @@ export function* fetchPortfolioWithScenarios(portfolioId) { 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, @@ -106,7 +109,8 @@ export function* onAddPortfolio(action) { export function* onUpdatePortfolio(action) { try { - const portfolio = yield call(updatePortfolio, action.portfolio._id, action.portfolio) + 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) @@ -115,7 +119,8 @@ export function* onUpdatePortfolio(action) { export function* onDeletePortfolio(action) { try { - yield call(deletePortfolio, action.id) + 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) 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 3158a219..ec679391 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js @@ -1,4 +1,4 @@ -import { call, put, select } from 'redux-saga/effects' +import { call, put, select, getContext } from 'redux-saga/effects' import { addToStore } from '../actions/objects' import { addPrefab } from '../../api/prefabs' import { getRackById } from './objects' @@ -7,7 +7,8 @@ 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 }) + const auth = yield getContext('auth') + const prefab = yield call(addPrefab, auth, { name: action.name, rack: currentRackJson }) yield put(addToStore('prefab', prefab)) } catch (error) { console.error(error) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/profile.js b/opendc-web/opendc-web-ui/src/redux/sagas/profile.js deleted file mode 100644 index e187b765..00000000 --- a/opendc-web/opendc-web-ui/src/redux/sagas/profile.js +++ /dev/null @@ -1,12 +0,0 @@ -import { call, put } from 'redux-saga/effects' -import { deleteCurrentUserSucceeded } from '../actions/users' -import { deleteUser } from '../../api/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/redux/sagas/projects.js b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js index ecd9a7c9..506df6ed 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/projects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js @@ -1,14 +1,15 @@ -import { call, put } from 'redux-saga/effects' +import { call, put, getContext } from 'redux-saga/effects' import { addToStore } from '../actions/objects' -import { addProjectSucceeded, deleteProjectSucceeded } from '../actions/projects' -import { addProject, deleteProject, getProject } from '../../api/projects' +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 project = yield call(getProject, action.id) + const auth = yield getContext('auth') + const project = yield call(getProject, auth, action.id) yield put(addToStore('project', project)) yield fetchAndStoreAllTopologiesOfProject(action.id, true) @@ -22,17 +23,10 @@ export function* onOpenProjectSucceeded(action) { export function* onProjectAdd(action) { try { - const project = yield call(addProject, { name: action.name }) + const auth = yield getContext('auth') + const project = yield call(addProject, auth, { 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])) + yield put(addProjectSucceeded(project)) } catch (error) { console.error(error) } @@ -40,9 +34,20 @@ export function* onProjectAdd(action) { export function* onProjectDelete(action) { try { - yield call(deleteProject, action.id) + 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 index a5463fa6..bdb7c45d 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js @@ -1,4 +1,4 @@ -import { call, put, select } from 'redux-saga/effects' +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' @@ -8,7 +8,8 @@ import { fetchPortfolioWithScenarios, watchForPortfolioResults } from './portfol export function* onOpenScenarioSucceeded(action) { try { - const project = yield call(getProject, action.projectId) + const auth = yield getContext('auth') + const project = yield call(getProject, auth, action.projectId) yield put(addToStore('project', project)) yield fetchAndStoreAllTopologiesOfProject(project._id) yield fetchAndStoreAllSchedulers() @@ -23,7 +24,8 @@ export function* onOpenScenarioSucceeded(action) { export function* onAddScenario(action) { try { - const scenario = yield call(addScenario, action.scenario.portfolioId, action.scenario) + 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) @@ -40,7 +42,8 @@ export function* onAddScenario(action) { export function* onUpdateScenario(action) { try { - const scenario = yield call(updateScenario, action.scenario._id, action.scenario) + 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) @@ -49,7 +52,8 @@ export function* onUpdateScenario(action) { export function* onDeleteScenario(action) { try { - yield call(deleteScenario, action.id) + 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) 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 65f97cc9..e5fd3d39 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -1,4 +1,4 @@ -import { call, put, select } from 'redux-saga/effects' +import { call, put, select, getContext } from 'redux-saga/effects' import { goDownOneInteractionLevel } from '../actions/interaction-level' import { addIdToStoreObjectListProp, @@ -50,8 +50,10 @@ export function* onAddTopology(action) { topologyToBeCreated = { name: action.name, rooms: [] } } + const auth = yield getContext('auth') const topology = yield call( addTopology, + auth, Object.assign({}, topologyToBeCreated, { projectId: currentProjectId, }) @@ -79,7 +81,8 @@ export function* onDeleteTopology(action) { yield put(setCurrentTopology(topologyIds.filter((t) => t !== action.id)[0])) } - yield call(deleteTopology, action.id) + const auth = yield getContext('auth') + yield call(deleteTopology, auth, action.id) yield put( addPropToStoreObject('project', currentProjectId, { diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/users.js b/opendc-web/opendc-web-ui/src/redux/sagas/users.js deleted file mode 100644 index 88c424b5..00000000 --- a/opendc-web/opendc-web-ui/src/redux/sagas/users.js +++ /dev/null @@ -1,44 +0,0 @@ -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/token-signin' -import { addUser } from '../../api/users' -import { saveAuthLocalStorage } from '../../auth' -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) - } -} -- cgit v1.2.3 From 5c582427366a30a21df64db393ecb9e4d1379439 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 17 May 2021 17:32:00 +0200 Subject: ui: Re-add support for Sentry This change re-adds the support for Sentry. This was lost during the migration from CRA to Next.js. --- opendc-web/opendc-web-ui/src/redux/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/index.js b/opendc-web/opendc-web-ui/src/redux/index.js index ee6ca3f5..5c908957 100644 --- a/opendc-web/opendc-web-ui/src/redux/index.js +++ b/opendc-web/opendc-web-ui/src/redux/index.js @@ -1,11 +1,12 @@ import { useMemo } from 'react' -import { applyMiddleware, createStore } from 'redux' +import {applyMiddleware, compose, createStore} from 'redux' import { createLogger } from 'redux-logger' import createSagaMiddleware from 'redux-saga' import thunk from 'redux-thunk' import rootReducer from './reducers' import rootSaga from './sagas' import { viewportAdjustmentMiddleware } from './middleware/viewport-adjustment' +import { createReduxEnhancer } from "@sentry/react"; let store @@ -18,7 +19,13 @@ function initStore(initialState, ctx) { middlewares.push(createLogger()) } - const configuredStore = createStore(rootReducer, initialState, applyMiddleware(...middlewares)) + let middleware = applyMiddleware(...middlewares) + + if (process.env.NEXT_PUBLIC_SENTRY_DSN) { + middleware = compose(middleware, createReduxEnhancer()) + } + + const configuredStore = createStore(rootReducer, initialState, middleware) sagaMiddleware.run(rootSaga) store = configuredStore -- cgit v1.2.3 From 0e52785dfc5e99f48718530976083cfbd1507651 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 5 Jul 2021 15:36:23 +0200 Subject: ui: Fix linting errors --- opendc-web/opendc-web-ui/src/redux/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/index.js b/opendc-web/opendc-web-ui/src/redux/index.js index 5c908957..3c7ad55f 100644 --- a/opendc-web/opendc-web-ui/src/redux/index.js +++ b/opendc-web/opendc-web-ui/src/redux/index.js @@ -1,12 +1,12 @@ import { useMemo } from 'react' -import {applyMiddleware, compose, createStore} from 'redux' +import { applyMiddleware, compose, createStore } from 'redux' import { createLogger } from 'redux-logger' import createSagaMiddleware from 'redux-saga' import thunk from 'redux-thunk' import rootReducer from './reducers' import rootSaga from './sagas' import { viewportAdjustmentMiddleware } from './middleware/viewport-adjustment' -import { createReduxEnhancer } from "@sentry/react"; +import { createReduxEnhancer } from '@sentry/react' let store -- cgit v1.2.3 From aa788a3ad18badfac8beaabdaffc88b9e52f9306 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 15:07:11 +0200 Subject: ui: Remove current ids state from Redux This change removes the current active identifiers from the Redux state. Instead, we use the router query to track the active project, portfolio and topology. --- .../opendc-web-ui/src/redux/actions/portfolios.js | 11 +---- .../opendc-web-ui/src/redux/actions/scenarios.js | 9 ---- .../opendc-web-ui/src/redux/actions/topologies.js | 3 +- .../src/redux/reducers/current-ids.js | 44 -------------------- .../opendc-web-ui/src/redux/reducers/index.js | 5 +-- .../opendc-web-ui/src/redux/sagas/portfolios.js | 48 +++++++++++----------- .../opendc-web-ui/src/redux/sagas/projects.js | 2 +- .../opendc-web-ui/src/redux/sagas/scenarios.js | 8 ++-- .../opendc-web-ui/src/redux/sagas/topology.js | 28 +++++-------- 9 files changed, 44 insertions(+), 114 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js b/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js index d37886d8..923cd217 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js @@ -2,11 +2,11 @@ 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) { +export function addPortfolio(projectId, portfolio) { return { type: ADD_PORTFOLIO, + projectId, portfolio, } } @@ -32,10 +32,3 @@ export function openPortfolioSucceeded(projectId, portfolioId) { portfolioId, } } - -export function setCurrentPortfolio(portfolioId) { - return { - type: SET_CURRENT_PORTFOLIO, - portfolioId, - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js b/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js index c8a90762..644933d6 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js @@ -2,7 +2,6 @@ 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 { @@ -33,11 +32,3 @@ export function openScenarioSucceeded(projectId, portfolioId, scenarioId) { 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..abfded7e 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topologies.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topologies.js @@ -1,9 +1,10 @@ export const ADD_TOPOLOGY = 'ADD_TOPOLOGY' export const DELETE_TOPOLOGY = 'DELETE_TOPOLOGY' -export function addTopology(name, duplicateId) { +export function addTopology(projectId, name, duplicateId) { return { type: ADD_TOPOLOGY, + projectId, name, duplicateId, } 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..9f556d18 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/index.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/index.js @@ -1,6 +1,6 @@ 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' @@ -11,10 +11,7 @@ const rootReducer = combineReducers({ projects, construction, map, - currentProjectId, currentTopologyId, - currentPortfolioId, - currentScenarioId, interactionLevel, }) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js index 340cb490..48d1ad3e 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js @@ -12,35 +12,37 @@ export function* onOpenPortfolioSucceeded(action) { const project = yield call(getProject, auth, action.projectId) yield put(addToStore('project', project)) yield fetchAndStoreAllTopologiesOfProject(project._id) - yield fetchPortfoliosOfProject() + yield fetchPortfoliosOfProject(project) yield fetchAndStoreAllSchedulers() yield fetchAndStoreAllTraces() - yield watchForPortfolioResults() + yield watchForPortfolioResults(action.portfolioId) } catch (error) { console.error(error) } } -export function* watchForPortfolioResults() { +export function* watchForPortfolioResults(portfolioId) { try { - const currentPortfolioId = yield select((state) => state.currentPortfolioId) - let unfinishedScenarios = yield getCurrentUnfinishedScenarios() + let unfinishedScenarios = yield getCurrentUnfinishedScenarios(portfolioId) while (unfinishedScenarios.length > 0) { yield delay(3000) - yield fetchPortfolioWithScenarios(currentPortfolioId) - unfinishedScenarios = yield getCurrentUnfinishedScenarios() + yield fetchPortfolioWithScenarios(portfolioId) + unfinishedScenarios = yield getCurrentUnfinishedScenarios(portfolioId) } } catch (error) { console.error(error) } } -export function* getCurrentUnfinishedScenarios() { +export function* getCurrentUnfinishedScenarios(portfolioId) { try { - const currentPortfolioId = yield select((state) => state.currentPortfolioId) - const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds) + if (!portfolioId) { + return [] + } + + const scenarioIds = yield select((state) => state.objects.portfolio[portfolioId].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') @@ -49,16 +51,13 @@ export function* getCurrentUnfinishedScenarios() { } } -export function* fetchPortfoliosOfProject() { +export function* fetchPortfoliosOfProject(project) { 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]) + for (const i in project.portfolioIds) { + yield fetchPortfolioWithScenarios(project.portfolioIds[i]) } } catch (error) { console.error(error) @@ -83,22 +82,22 @@ export function* fetchPortfolioWithScenarios(portfolioId) { export function* onAddPortfolio(action) { try { - const currentProjectId = yield select((state) => state.currentProjectId) + const { projectId } = action const auth = yield getContext('auth') const portfolio = yield call( addPortfolio, auth, - currentProjectId, + projectId, Object.assign({}, action.portfolio, { - projectId: currentProjectId, + projectId: projectId, scenarioIds: [], }) ) yield put(addToStore('portfolio', portfolio)) - const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds) + const portfolioIds = yield select((state) => state.objects.project[projectId].portfolioIds) yield put( - addPropToStoreObject('project', currentProjectId, { + addPropToStoreObject('project', projectId, { portfolioIds: portfolioIds.concat([portfolio._id]), }) ) @@ -120,13 +119,14 @@ export function* onUpdatePortfolio(action) { export function* onDeletePortfolio(action) { try { const auth = yield getContext('auth') + const portfolio = yield select((state) => state.objects.portfolio[action.id]) + yield call(deletePortfolio, auth, action.id) - const currentProjectId = yield select((state) => state.currentProjectId) - const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds) + const portfolioIds = yield select((state) => state.objects.project[portfolio.projectId].portfolioIds) yield put( - addPropToStoreObject('project', currentProjectId, { + addPropToStoreObject('project', portfolio.projectId, { portfolioIds: portfolioIds.filter((id) => id !== action.id), }) ) 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..0689090a 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/projects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js @@ -13,7 +13,7 @@ export function* onOpenProjectSucceeded(action) { yield put(addToStore('project', project)) yield fetchAndStoreAllTopologiesOfProject(action.id, true) - yield fetchPortfoliosOfProject() + yield fetchPortfoliosOfProject(project) yield fetchAndStoreAllSchedulers() yield fetchAndStoreAllTraces() } catch (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 index bdb7c45d..b2979636 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js @@ -34,7 +34,7 @@ export function* onAddScenario(action) { scenarioIds: scenarioIds.concat([scenario._id]), }) ) - yield watchForPortfolioResults() + yield watchForPortfolioResults(action.scenario.portfolioId) } catch (error) { console.error(error) } @@ -53,13 +53,13 @@ export function* onUpdateScenario(action) { export function* onDeleteScenario(action) { try { const auth = yield getContext('auth') + const scenario = yield select((state) => state.objects.scenario[action.id]) yield call(deleteScenario, auth, action.id) - const currentPortfolioId = yield select((state) => state.currentPortfolioId) - const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds) + const scenarioIds = yield select((state) => state.objects.portfolio[scenario.portfolioId].scenarioIds) yield put( - addPropToStoreObject('scenario', currentPortfolioId, { + addPropToStoreObject('scenario', scenario.portfolioId, { scenarioIds: scenarioIds.filter((id) => id !== action.id), }) ) 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..4f7bc8db 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -38,31 +38,23 @@ 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 getTopologyAsObject(duplicateId, false) + topologyToBeCreated = { ...topologyToBeCreated, name } } 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) + const topologyIds = yield select((state) => state.objects.project[projectId].topologyIds) yield put( - addPropToStoreObject('project', currentProjectId, { + addPropToStoreObject('project', projectId, { topologyIds: topologyIds.concat([topology._id]), }) ) @@ -74,8 +66,8 @@ export function* onAddTopology(action) { export function* onDeleteTopology(action) { try { - const currentProjectId = yield select((state) => state.currentProjectId) - const topologyIds = yield select((state) => state.objects.project[currentProjectId].topologyIds) + const topology = yield select((state) => state.objects.topologies[action.id]) + const topologyIds = yield select((state) => state.objects.project[topology.projectId].topologyIds) const currentTopologyId = yield select((state) => state.currentTopologyId) if (currentTopologyId === action.id) { yield put(setCurrentTopology(topologyIds.filter((t) => t !== action.id)[0])) @@ -85,7 +77,7 @@ export function* onDeleteTopology(action) { yield call(deleteTopology, auth, action.id) yield put( - addPropToStoreObject('project', currentProjectId, { + addPropToStoreObject('project', topology.projectId, { topologyIds: topologyIds.filter((id) => id !== action.id), }) ) -- cgit v1.2.3 From e5e5d2c65e583493870bc0b62fb185c5e757c13f Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 16:27:49 +0200 Subject: ui: Migrate project APIs to React Query This change updates the OpenDC frontend to use React Query for fetching and mutating project data. Previously, this state was tracked and synchronized via Redux. Migrating to React Query greatly simplifies the state synchronization logic necessary in the frontend. --- .../opendc-web-ui/src/redux/actions/projects.js | 47 ---------------------- .../opendc-web-ui/src/redux/reducers/index.js | 2 - .../src/redux/reducers/interaction-level.js | 2 - .../opendc-web-ui/src/redux/reducers/projects.js | 14 ------- opendc-web/opendc-web-ui/src/redux/sagas/index.js | 6 +-- .../opendc-web-ui/src/redux/sagas/objects.js | 7 ---- .../opendc-web-ui/src/redux/sagas/portfolios.js | 29 ++++--------- .../opendc-web-ui/src/redux/sagas/projects.js | 43 +++----------------- .../opendc-web-ui/src/redux/sagas/scenarios.js | 7 +++- .../opendc-web-ui/src/redux/sagas/topology.js | 30 ++++++-------- 10 files changed, 32 insertions(+), 155 deletions(-) delete mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/projects.js (limited to 'opendc-web/opendc-web-ui/src/redux') 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/reducers/index.js b/opendc-web/opendc-web-ui/src/redux/reducers/index.js index 9f556d18..1b17a206 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/index.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/index.js @@ -4,11 +4,9 @@ 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, currentTopologyId, 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..8bf81b98 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 @@ -5,7 +5,6 @@ import { 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' @@ -13,7 +12,6 @@ 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/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..939be691 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/index.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/index.js @@ -11,7 +11,7 @@ import { ADD_UNIT, DELETE_MACHINE, DELETE_UNIT } from '../actions/topology/machi 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, @@ -36,10 +36,6 @@ 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) 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..5523dd57 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js @@ -1,13 +1,11 @@ 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, @@ -41,11 +39,6 @@ function* fetchAndStoreObjects(objectType, apiCall) { return objects } -export const fetchAndStoreProject = function* (id) { - const auth = yield getContext('auth') - return yield fetchAndStoreObject('project', id, call(getProject, auth, id)) -} - export const fetchAndStoreTopology = function* (id) { const topologyStore = yield select(OBJECT_SELECTORS['topology']) const roomStore = yield select(OBJECT_SELECTORS['room']) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js index 48d1ad3e..c32fcdc0 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js @@ -1,7 +1,7 @@ import { call, put, select, delay, getContext } from 'redux-saga/effects' -import { addPropToStoreObject, addToStore } from '../actions/objects' +import { addToStore } from '../actions/objects' import { addPortfolio, deletePortfolio, getPortfolio, updatePortfolio } from '../../api/portfolios' -import { getProject } from '../../api/projects' +import { fetchProject } from '../../api/projects' import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' import { fetchAndStoreAllTopologiesOfProject } from './topology' import { getScenario } from '../../api/scenarios' @@ -9,9 +9,11 @@ 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) + const queryClient = yield getContext('queryClient') + const project = yield call(() => + queryClient.fetchQuery(`projects/${action.projectId}`, () => fetchProject(auth, action.projectId)) + ) + yield fetchAndStoreAllTopologiesOfProject(action.projectId) yield fetchPortfoliosOfProject(project) yield fetchAndStoreAllSchedulers() yield fetchAndStoreAllTraces() @@ -94,13 +96,6 @@ export function* onAddPortfolio(action) { }) ) yield put(addToStore('portfolio', portfolio)) - - const portfolioIds = yield select((state) => state.objects.project[projectId].portfolioIds) - yield put( - addPropToStoreObject('project', projectId, { - portfolioIds: portfolioIds.concat([portfolio._id]), - }) - ) } catch (error) { console.error(error) } @@ -119,17 +114,7 @@ export function* onUpdatePortfolio(action) { export function* onDeletePortfolio(action) { try { const auth = yield getContext('auth') - const portfolio = yield select((state) => state.objects.portfolio[action.id]) - yield call(deletePortfolio, auth, action.id) - - const portfolioIds = yield select((state) => state.objects.project[portfolio.projectId].portfolioIds) - - yield put( - addPropToStoreObject('project', portfolio.projectId, { - portfolioIds: portfolioIds.filter((id) => id !== action.id), - }) - ) } 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 0689090a..96a4323c 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/projects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js @@ -1,16 +1,16 @@ -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 { call, getContext } from 'redux-saga/effects' import { fetchAndStoreAllTopologiesOfProject } from './topology' import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' import { fetchPortfoliosOfProject } from './portfolios' +import { fetchProject } from '../../api/projects' export function* onOpenProjectSucceeded(action) { try { const auth = yield getContext('auth') - const project = yield call(getProject, auth, action.id) - yield put(addToStore('project', project)) + const queryClient = yield getContext('queryClient') + const project = yield call(() => + queryClient.fetchQuery(`projects/${action.id}`, () => fetchProject(auth, action.id)) + ) yield fetchAndStoreAllTopologiesOfProject(action.id, true) yield fetchPortfoliosOfProject(project) @@ -20,34 +20,3 @@ export function* onOpenProjectSucceeded(action) { 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 index b2979636..3fe12981 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js @@ -1,6 +1,6 @@ import { call, put, select, getContext } from 'redux-saga/effects' import { addPropToStoreObject, addToStore } from '../actions/objects' -import { getProject } from '../../api/projects' +import { fetchProject } from '../../api/projects' import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' import { fetchAndStoreAllTopologiesOfProject } from './topology' import { addScenario, deleteScenario, updateScenario } from '../../api/scenarios' @@ -9,7 +9,10 @@ import { fetchPortfolioWithScenarios, watchForPortfolioResults } from './portfol export function* onOpenScenarioSucceeded(action) { try { const auth = yield getContext('auth') - const project = yield call(getProject, auth, action.projectId) + const queryClient = yield getContext('queryClient') + const project = yield call(() => + queryClient.fetchQuery(`projects/${action.projectId}`, () => fetchProject(auth, action.projectId)) + ) yield put(addToStore('project', project)) yield fetchAndStoreAllTopologiesOfProject(project._id) yield fetchAndStoreAllSchedulers() 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 4f7bc8db..3f41e1d4 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -19,10 +19,15 @@ import { import { fetchAndStoreTopology, getTopologyAsObject, updateTopologyOnServer } from './objects' import { uuid } from 'uuidv4' import { addTopology, deleteTopology } from '../../api/topologies' +import { fetchProject } from '../../api/projects' export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = false) { try { - const project = yield select((state) => state.objects.project[projectId]) + const auth = yield getContext('auth') + const queryClient = yield getContext('queryClient') + const project = yield call(() => + queryClient.fetchQuery(`projects/${projectId}`, () => fetchProject(auth, projectId)) + ) for (let i in project.topologyIds) { yield fetchAndStoreTopology(project.topologyIds[i]) @@ -51,13 +56,6 @@ export function* onAddTopology(action) { const auth = yield getContext('auth') const topology = yield call(addTopology, auth, { ...topologyToBeCreated, projectId }) yield fetchAndStoreTopology(topology._id) - - const topologyIds = yield select((state) => state.objects.project[projectId].topologyIds) - yield put( - addPropToStoreObject('project', projectId, { - topologyIds: topologyIds.concat([topology._id]), - }) - ) yield put(setCurrentTopology(topology._id)) } catch (error) { console.error(error) @@ -66,21 +64,19 @@ export function* onAddTopology(action) { export function* onDeleteTopology(action) { try { - const topology = yield select((state) => state.objects.topologies[action.id]) - const topologyIds = yield select((state) => state.objects.project[topology.projectId].topologyIds) + const auth = yield getContext('auth') + const queryClient = yield getContext('queryClient') + const project = yield call(() => + queryClient.fetchQuery(`projects/${action.projectId}`, () => fetchProject(auth, action.projectId)) + ) + const topologyIds = project?.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', topology.projectId, { - topologyIds: topologyIds.filter((id) => id !== action.id), - }) - ) } catch (error) { console.error(error) } -- cgit v1.2.3 From d28a2f194a75eb86095485ae4f88be349bcc18b6 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 16:36:54 +0200 Subject: ui: Fetch schedulers and traces using React Query This change updates the OpenDC frontend to fetch schedulers and traces using React Query, removing its dependency on Redux. --- .../opendc-web-ui/src/redux/reducers/objects.js | 2 -- opendc-web/opendc-web-ui/src/redux/sagas/objects.js | 19 ++----------------- .../opendc-web-ui/src/redux/sagas/portfolios.js | 7 ------- opendc-web/opendc-web-ui/src/redux/sagas/projects.js | 3 --- opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js | 3 --- 5 files changed, 2 insertions(+), 32 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/redux') 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..f8f577ac 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/objects.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/objects.js @@ -20,8 +20,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'), 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 5523dd57..fe826014 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js @@ -1,7 +1,7 @@ import { call, put, select, getContext } from 'redux-saga/effects' import { addToStore } from '../actions/objects' -import { getAllSchedulers } from '../../api/schedulers' -import { getAllTraces } from '../../api/traces' +import { fetchSchedulers } from '../../api/schedulers' +import { fetchTraces } from '../../api/traces' import { getTopology, updateTopology } from '../../api/topologies' import { uuid } from 'uuidv4' @@ -210,18 +210,3 @@ export const getRackById = function* (id, keepIds) { })), } } - -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 -} diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js index c32fcdc0..68956225 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js @@ -2,8 +2,6 @@ import { call, put, select, delay, getContext } from 'redux-saga/effects' import { addToStore } from '../actions/objects' import { addPortfolio, deletePortfolio, getPortfolio, updatePortfolio } from '../../api/portfolios' import { fetchProject } from '../../api/projects' -import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' -import { fetchAndStoreAllTopologiesOfProject } from './topology' import { getScenario } from '../../api/scenarios' export function* onOpenPortfolioSucceeded(action) { @@ -15,8 +13,6 @@ export function* onOpenPortfolioSucceeded(action) { ) yield fetchAndStoreAllTopologiesOfProject(action.projectId) yield fetchPortfoliosOfProject(project) - yield fetchAndStoreAllSchedulers() - yield fetchAndStoreAllTraces() yield watchForPortfolioResults(action.portfolioId) } catch (error) { @@ -55,9 +51,6 @@ export function* getCurrentUnfinishedScenarios(portfolioId) { export function* fetchPortfoliosOfProject(project) { try { - yield fetchAndStoreAllSchedulers() - yield fetchAndStoreAllTraces() - for (const i in project.portfolioIds) { yield fetchPortfolioWithScenarios(project.portfolioIds[i]) } 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 96a4323c..6dc3c682 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/projects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js @@ -1,6 +1,5 @@ import { call, getContext } from 'redux-saga/effects' import { fetchAndStoreAllTopologiesOfProject } from './topology' -import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' import { fetchPortfoliosOfProject } from './portfolios' import { fetchProject } from '../../api/projects' @@ -14,8 +13,6 @@ export function* onOpenProjectSucceeded(action) { yield fetchAndStoreAllTopologiesOfProject(action.id, true) yield fetchPortfoliosOfProject(project) - yield fetchAndStoreAllSchedulers() - yield fetchAndStoreAllTraces() } 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 index 3fe12981..10ab3547 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js @@ -1,7 +1,6 @@ import { call, put, select, getContext } from 'redux-saga/effects' import { addPropToStoreObject, addToStore } from '../actions/objects' import { fetchProject } from '../../api/projects' -import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' import { fetchAndStoreAllTopologiesOfProject } from './topology' import { addScenario, deleteScenario, updateScenario } from '../../api/scenarios' import { fetchPortfolioWithScenarios, watchForPortfolioResults } from './portfolios' @@ -15,8 +14,6 @@ export function* onOpenScenarioSucceeded(action) { ) yield put(addToStore('project', project)) yield fetchAndStoreAllTopologiesOfProject(project._id) - yield fetchAndStoreAllSchedulers() - yield fetchAndStoreAllTraces() yield fetchPortfolioWithScenarios(action.portfolioId) // TODO Fetch scenario-specific metrics -- cgit v1.2.3 From 9c8a987556d0fb0cdf0eb67e0c191a8dcc5593b9 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 17:30:15 +0200 Subject: ui: Fetch scenarios and portfolios using React Query --- .../opendc-web-ui/src/redux/actions/portfolios.js | 34 ------ .../opendc-web-ui/src/redux/actions/scenarios.js | 34 ------ .../src/redux/middleware/viewport-adjustment.js | 2 +- .../src/redux/reducers/construction-mode.js | 6 -- .../src/redux/reducers/interaction-level.js | 4 - opendc-web/opendc-web-ui/src/redux/sagas/index.js | 16 +-- .../opendc-web-ui/src/redux/sagas/objects.js | 30 ++---- .../opendc-web-ui/src/redux/sagas/portfolios.js | 114 --------------------- .../opendc-web-ui/src/redux/sagas/projects.js | 10 -- .../opendc-web-ui/src/redux/sagas/scenarios.js | 69 ------------- .../opendc-web-ui/src/redux/sagas/topology.js | 8 +- 11 files changed, 14 insertions(+), 313 deletions(-) delete mode 100644 opendc-web/opendc-web-ui/src/redux/actions/portfolios.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/actions/scenarios.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js (limited to 'opendc-web/opendc-web-ui/src/redux') 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 923cd217..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/portfolios.js +++ /dev/null @@ -1,34 +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 function addPortfolio(projectId, portfolio) { - return { - type: ADD_PORTFOLIO, - projectId, - 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, - } -} 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 644933d6..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/scenarios.js +++ /dev/null @@ -1,34 +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 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, - } -} 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..bee6becd 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,7 +22,7 @@ export const viewportAdjustmentMiddleware = (store) => (next) => (action) => { mapDimensions = { width: action.width, height: action.height } } - if (topologyId !== '-1') { + if (topologyId && topologyId !== '-1') { const roomIds = state.objects.topology[topologyId].roomIds const rooms = roomIds.map((id) => Object.assign({}, state.objects.room[id])) rooms.forEach((room) => (room.tiles = room.tileIds.map((tileId) => state.objects.tile[tileId]))) 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/interaction-level.js b/opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js index 8bf81b98..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,4 +1,3 @@ -import { OPEN_PORTFOLIO_SUCCEEDED } from '../actions/portfolios' import { GO_DOWN_ONE_INTERACTION_LEVEL, GO_FROM_BUILDING_TO_ROOM, @@ -6,12 +5,9 @@ import { GO_FROM_ROOM_TO_RACK, } from '../actions/interaction-level' 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 SET_CURRENT_TOPOLOGY: return { mode: 'BUILDING', 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 939be691..74d9efb6 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,7 +9,6 @@ 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 { onOpenProjectSucceeded } from './projects' import { onAddMachine, @@ -30,15 +28,11 @@ import { 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 { onAddPrefab } from './prefabs' import { ADD_PREFAB } from '../actions/prefabs' export default function* rootSaga() { 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) @@ -56,13 +50,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 fe826014..88f71fe4 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js @@ -1,7 +1,5 @@ import { call, put, select, getContext } from 'redux-saga/effects' import { addToStore } from '../actions/objects' -import { fetchSchedulers } from '../../api/schedulers' -import { fetchTraces } from '../../api/traces' import { getTopology, updateTopology } from '../../api/topologies' import { uuid } from 'uuidv4' @@ -21,24 +19,9 @@ export const OBJECT_SELECTORS = { 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 -} - +/** + * 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']) @@ -135,12 +118,15 @@ const generateIdIfNotPresent = (obj) => { } export const updateTopologyOnServer = function* (id) { - const topology = yield getTopologyAsObject(id, true) + const topology = yield denormalizeTopology(id, true) const auth = yield getContext('auth') yield call(updateTopology, auth, topology) } -export const getTopologyAsObject = function* (id, keepIds) { +/** + * Denormalizes the topology representation in order to be stored on the server. + */ +export const denormalizeTopology = function* (id, keepIds) { const topologyStore = yield select(OBJECT_SELECTORS['topology']) const rooms = yield getAllRooms(topologyStore[id].roomIds, keepIds) return { 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 68956225..00000000 --- a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js +++ /dev/null @@ -1,114 +0,0 @@ -import { call, put, select, delay, getContext } from 'redux-saga/effects' -import { addToStore } from '../actions/objects' -import { addPortfolio, deletePortfolio, getPortfolio, updatePortfolio } from '../../api/portfolios' -import { fetchProject } from '../../api/projects' -import { getScenario } from '../../api/scenarios' - -export function* onOpenPortfolioSucceeded(action) { - try { - const auth = yield getContext('auth') - const queryClient = yield getContext('queryClient') - const project = yield call(() => - queryClient.fetchQuery(`projects/${action.projectId}`, () => fetchProject(auth, action.projectId)) - ) - yield fetchAndStoreAllTopologiesOfProject(action.projectId) - yield fetchPortfoliosOfProject(project) - - yield watchForPortfolioResults(action.portfolioId) - } catch (error) { - console.error(error) - } -} - -export function* watchForPortfolioResults(portfolioId) { - try { - let unfinishedScenarios = yield getCurrentUnfinishedScenarios(portfolioId) - - while (unfinishedScenarios.length > 0) { - yield delay(3000) - yield fetchPortfolioWithScenarios(portfolioId) - unfinishedScenarios = yield getCurrentUnfinishedScenarios(portfolioId) - } - } catch (error) { - console.error(error) - } -} - -export function* getCurrentUnfinishedScenarios(portfolioId) { - try { - if (!portfolioId) { - return [] - } - - const scenarioIds = yield select((state) => state.objects.portfolio[portfolioId].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(project) { - try { - for (const i in project.portfolioIds) { - yield fetchPortfolioWithScenarios(project.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 { projectId } = action - const auth = yield getContext('auth') - const portfolio = yield call( - addPortfolio, - auth, - projectId, - Object.assign({}, action.portfolio, { - projectId: projectId, - scenarioIds: [], - }) - ) - yield put(addToStore('portfolio', portfolio)) - } 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) - } 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 6dc3c682..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,18 +1,8 @@ -import { call, getContext } from 'redux-saga/effects' import { fetchAndStoreAllTopologiesOfProject } from './topology' -import { fetchPortfoliosOfProject } from './portfolios' -import { fetchProject } from '../../api/projects' export function* onOpenProjectSucceeded(action) { try { - const auth = yield getContext('auth') - const queryClient = yield getContext('queryClient') - const project = yield call(() => - queryClient.fetchQuery(`projects/${action.id}`, () => fetchProject(auth, action.id)) - ) - yield fetchAndStoreAllTopologiesOfProject(action.id, true) - yield fetchPortfoliosOfProject(project) } 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 10ab3547..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 { fetchProject } from '../../api/projects' -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 queryClient = yield getContext('queryClient') - const project = yield call(() => - queryClient.fetchQuery(`projects/${action.projectId}`, () => fetchProject(auth, action.projectId)) - ) - yield put(addToStore('project', project)) - yield fetchAndStoreAllTopologiesOfProject(project._id) - 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(action.scenario.portfolioId) - } 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') - const scenario = yield select((state) => state.objects.scenario[action.id]) - yield call(deleteScenario, auth, action.id) - - const scenarioIds = yield select((state) => state.objects.portfolio[scenario.portfolioId].scenarioIds) - - yield put( - addPropToStoreObject('scenario', scenario.portfolioId, { - 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 3f41e1d4..efa125c6 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -16,7 +16,7 @@ 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 { fetchProject } from '../../api/projects' @@ -26,7 +26,7 @@ export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = fa const auth = yield getContext('auth') const queryClient = yield getContext('queryClient') const project = yield call(() => - queryClient.fetchQuery(`projects/${projectId}`, () => fetchProject(auth, projectId)) + queryClient.fetchQuery(['projects', projectId], () => fetchProject(auth, projectId)) ) for (let i in project.topologyIds) { @@ -47,7 +47,7 @@ export function* onAddTopology(action) { let topologyToBeCreated if (duplicateId) { - topologyToBeCreated = yield getTopologyAsObject(duplicateId, false) + topologyToBeCreated = yield denormalizeTopology(duplicateId, false) topologyToBeCreated = { ...topologyToBeCreated, name } } else { topologyToBeCreated = { name: action.name, rooms: [] } @@ -67,7 +67,7 @@ export function* onDeleteTopology(action) { const auth = yield getContext('auth') const queryClient = yield getContext('queryClient') const project = yield call(() => - queryClient.fetchQuery(`projects/${action.projectId}`, () => fetchProject(auth, action.projectId)) + queryClient.fetchQuery(['projects', action.projectId], () => fetchProject(auth, action.projectId)) ) const topologyIds = project?.topologyIds ?? [] -- cgit v1.2.3 From 02a2f0f89cb1f39a5f8856bca1971a4e1b12374f Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 20:13:30 +0200 Subject: ui: Use React Query defaults to reduce duplication --- .../opendc-web-ui/src/redux/actions/topologies.js | 8 ------- opendc-web/opendc-web-ui/src/redux/sagas/index.js | 4 +--- .../opendc-web-ui/src/redux/sagas/objects.js | 4 ++-- .../opendc-web-ui/src/redux/sagas/topology.js | 28 ++-------------------- 4 files changed, 5 insertions(+), 39 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/redux') 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 abfded7e..e19a7f21 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topologies.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topologies.js @@ -1,5 +1,4 @@ export const ADD_TOPOLOGY = 'ADD_TOPOLOGY' -export const DELETE_TOPOLOGY = 'DELETE_TOPOLOGY' export function addTopology(projectId, name, duplicateId) { return { @@ -9,10 +8,3 @@ export function addTopology(projectId, name, duplicateId) { duplicateId, } } - -export function deleteTopology(id) { - return { - type: DELETE_TOPOLOGY, - id, - } -} 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 74d9efb6..318f0afb 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/index.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/index.js @@ -21,13 +21,12 @@ import { onDeleteRack, onDeleteRoom, onDeleteTile, - onDeleteTopology, onDeleteUnit, onEditRackName, onEditRoomName, onStartNewRoomConstruction, } from './topology' -import { ADD_TOPOLOGY, DELETE_TOPOLOGY } from '../actions/topologies' +import { ADD_TOPOLOGY } from '../actions/topologies' import { onAddPrefab } from './prefabs' import { ADD_PREFAB } from '../actions/prefabs' @@ -35,7 +34,6 @@ export default function* rootSaga() { yield takeEvery(OPEN_PROJECT_SUCCEEDED, onOpenProjectSucceeded) 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) 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 88f71fe4..99082df0 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js @@ -1,6 +1,6 @@ import { call, put, select, getContext } from 'redux-saga/effects' import { addToStore } from '../actions/objects' -import { getTopology, updateTopology } from '../../api/topologies' +import { fetchTopology, updateTopology } from '../../api/topologies' import { uuid } from 'uuidv4' export const OBJECT_SELECTORS = { @@ -32,7 +32,7 @@ export const fetchAndStoreTopology = function* (id) { let topology = topologyStore[id] if (!topology) { - const fullTopology = yield call(getTopology, auth, id) + const fullTopology = yield call(fetchTopology, auth, id) for (let roomIdx in fullTopology.rooms) { const fullRoom = fullTopology.rooms[roomIdx] 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 efa125c6..0ed40131 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -18,16 +18,12 @@ import { } from '../../components/app/map/MapConstants' import { fetchAndStoreTopology, denormalizeTopology, updateTopologyOnServer } from './objects' import { uuid } from 'uuidv4' -import { addTopology, deleteTopology } from '../../api/topologies' -import { fetchProject } from '../../api/projects' +import { addTopology } from '../../api/topologies' export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = false) { try { - const auth = yield getContext('auth') const queryClient = yield getContext('queryClient') - const project = yield call(() => - queryClient.fetchQuery(['projects', projectId], () => fetchProject(auth, projectId)) - ) + const project = yield call(() => queryClient.fetchQuery(['projects', projectId])) for (let i in project.topologyIds) { yield fetchAndStoreTopology(project.topologyIds[i]) @@ -62,26 +58,6 @@ export function* onAddTopology(action) { } } -export function* onDeleteTopology(action) { - try { - const auth = yield getContext('auth') - const queryClient = yield getContext('queryClient') - const project = yield call(() => - queryClient.fetchQuery(['projects', action.projectId], () => fetchProject(auth, action.projectId)) - ) - const topologyIds = project?.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, auth, action.id) - } catch (error) { - console.error(error) - } -} - export function* onStartNewRoomConstruction() { try { const topologyId = yield select((state) => state.currentTopologyId) -- cgit v1.2.3 From 29196842447d841d2e21462adcfc8c2ed1d851ad Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 8 Jul 2021 13:15:28 +0200 Subject: ui: Simplify normalization of topology This change updates the OpenDC frontend to use the normalizr library for normalizing the user topology. --- .../opendc-web-ui/src/redux/actions/topologies.js | 8 + .../src/redux/actions/topology/building.js | 13 +- .../src/redux/actions/topology/room.js | 2 +- .../src/redux/middleware/viewport-adjustment.js | 4 +- .../opendc-web-ui/src/redux/reducers/objects.js | 18 +- .../opendc-web-ui/src/redux/sagas/objects.js | 186 ++------------------- .../opendc-web-ui/src/redux/sagas/prefabs.js | 11 +- .../opendc-web-ui/src/redux/sagas/topology.js | 74 ++++---- 8 files changed, 80 insertions(+), 236 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/redux') 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 e19a7f21..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,4 +1,5 @@ export const ADD_TOPOLOGY = 'ADD_TOPOLOGY' +export const STORE_TOPOLOGY = 'STORE_TOPOLOGY' export function addTopology(projectId, name, duplicateId) { return { @@ -8,3 +9,10 @@ export function addTopology(projectId, name, duplicateId) { duplicateId, } } + +export function storeTopology(entities) { + return { + 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 bee6becd..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 @@ -23,9 +23,9 @@ export const viewportAdjustmentMiddleware = (store) => (next) => (action) => { } if (topologyId && topologyId !== '-1') { - const roomIds = state.objects.topology[topologyId].roomIds + 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/objects.js b/opendc-web/opendc-web-ui/src/redux/reducers/objects.js index f8f577ac..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,8 +18,6 @@ export const objects = combineReducers({ tile: object('tile'), room: object('room'), topology: object('topology'), - portfolio: object('portfolio'), - scenario: object('scenario'), prefab: object('prefab'), }) @@ -31,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/sagas/objects.js b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js index 99082df0..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,124 +1,27 @@ import { call, put, select, getContext } from 'redux-saga/effects' -import { addToStore } from '../actions/objects' import { fetchTopology, updateTopology } from '../../api/topologies' -import { uuid } from 'uuidv4' - -export const OBJECT_SELECTORS = { - 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, -} +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(fetchTopology, 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 denormalizeTopology(id, true) + const topology = yield denormalizeTopology(id) const auth = yield getContext('auth') yield call(updateTopology, auth, topology) } @@ -126,73 +29,8 @@ export const updateTopologyOnServer = function* (id) { /** * Denormalizes the topology representation in order to be stored on the server. */ -export const denormalizeTopology = 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 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/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/topology.js b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js index 0ed40131..5d9154fd 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -25,8 +25,8 @@ export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = fa 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) { @@ -43,8 +43,9 @@ export function* onAddTopology(action) { let topologyToBeCreated if (duplicateId) { - topologyToBeCreated = yield denormalizeTopology(duplicateId, false) + topologyToBeCreated = yield denormalizeTopology(duplicateId) topologyToBeCreated = { ...topologyToBeCreated, name } + delete topologyToBeCreated._id } else { topologyToBeCreated = { name: action.name, rooms: [] } } @@ -65,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) { @@ -80,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()) @@ -100,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) @@ -111,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) @@ -136,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) @@ -146,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 })) @@ -161,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) @@ -176,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) @@ -189,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) @@ -217,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) @@ -260,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) -- cgit v1.2.3 From e200dbfdc076ac6263c9ac6f9dabdcc475f01d6e Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sat, 10 Jul 2021 12:41:00 +0200 Subject: fix(ui): Relax topology schema requirements This change fixes an issue where the topology generated by the frontend was not accepted by the API server. --- opendc-web/opendc-web-ui/src/redux/sagas/topology.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js index 5d9154fd..333c1485 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -123,8 +123,6 @@ export function* onEditRoomName(action) { try { const topologyId = yield select((state) => state.currentTopologyId) const roomId = yield select((state) => state.interactionLevel.roomId) - const room = Object.assign({}, yield select((state) => state.objects.room[roomId])) - room.name = action.name yield put(addPropToStoreObject('room', roomId, { name: action.name })) yield updateTopologyOnServer(topologyId) } catch (error) { @@ -148,8 +146,6 @@ export function* onEditRackName(action) { try { const topologyId = yield select((state) => state.currentTopologyId) const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rack) - const rack = Object.assign({}, yield select((state) => state.objects.rack[rackId])) - rack.name = action.name yield put(addPropToStoreObject('rack', rackId, { name: action.name })) yield updateTopologyOnServer(topologyId) } catch (error) { @@ -175,6 +171,7 @@ export function* onAddRackToTile(action) { const rack = { _id: uuid(), name: 'Rack', + tileId: action.tileId, capacity: DEFAULT_RACK_SLOT_CAPACITY, powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, machines: [], @@ -195,6 +192,7 @@ export function* onAddMachine(action) { const machine = { _id: uuid(), + rackId, position: action.position, cpus: [], gpus: [], -- cgit v1.2.3 From f2aeecccc096728d3df955b71e711c8d9c429427 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Fri, 16 Jul 2021 17:37:01 +0200 Subject: refactor(ui): Isolate world coordinate space This change updates the topology view in the OpenDC frontend to isolate the world coordinate space. This means that zooming and panning should not affect the coordinates in world space (but only in camera space). In turn, this allows us to remove the dependency on Redux for the camera controls. --- opendc-web/opendc-web-ui/src/redux/actions/map.js | 83 ---------------------- opendc-web/opendc-web-ui/src/redux/index.js | 3 +- .../src/redux/middleware/viewport-adjustment.js | 73 ------------------- .../opendc-web-ui/src/redux/reducers/index.js | 2 - opendc-web/opendc-web-ui/src/redux/reducers/map.js | 35 --------- 5 files changed, 1 insertion(+), 195 deletions(-) delete mode 100644 opendc-web/opendc-web-ui/src/redux/actions/map.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/map.js (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/actions/map.js b/opendc-web/opendc-web-ui/src/redux/actions/map.js deleted file mode 100644 index aa14cacd..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/map.js +++ /dev/null @@ -1,83 +0,0 @@ -import { - MAP_MAX_SCALE, - MAP_MIN_SCALE, - MAP_SCALE_PER_EVENT, - MAP_SIZE_IN_PIXELS, -} from '../../components/app/map/MapConstants' - -export const SET_MAP_POSITION = 'SET_MAP_POSITION' -export const SET_MAP_DIMENSIONS = 'SET_MAP_DIMENSIONS' -export const SET_MAP_SCALE = 'SET_MAP_SCALE' - -export function setMapPosition(x, y) { - return { - type: SET_MAP_POSITION, - x, - y, - } -} - -export function setMapDimensions(width, height) { - return { - type: SET_MAP_DIMENSIONS, - width, - height, - } -} - -export function setMapScale(scale) { - return { - type: SET_MAP_SCALE, - scale, - } -} - -export function zoomInOnCenter(zoomIn) { - return (dispatch, getState) => { - const state = getState() - - dispatch(zoomInOnPosition(zoomIn, state.map.dimensions.width / 2, state.map.dimensions.height / 2)) - } -} - -export function zoomInOnPosition(zoomIn, x, y) { - return (dispatch, getState) => { - const state = getState() - - const centerPoint = { - x: x / state.map.scale - state.map.position.x / state.map.scale, - y: y / state.map.scale - state.map.position.y / state.map.scale, - } - const newScale = zoomIn ? state.map.scale * MAP_SCALE_PER_EVENT : state.map.scale / MAP_SCALE_PER_EVENT - const boundedScale = Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE) - - const newX = -(centerPoint.x - x / boundedScale) * boundedScale - const newY = -(centerPoint.y - y / boundedScale) * boundedScale - - dispatch(setMapPositionWithBoundsCheck(newX, newY)) - dispatch(setMapScale(boundedScale)) - } -} - -export function setMapPositionWithBoundsCheck(x, y) { - return (dispatch, getState) => { - const state = getState() - - const scaledMapSize = MAP_SIZE_IN_PIXELS * state.map.scale - - const updatedX = - x > 0 - ? 0 - : x < -scaledMapSize + state.map.dimensions.width - ? -scaledMapSize + state.map.dimensions.width - : x - const updatedY = - y > 0 - ? 0 - : y < -scaledMapSize + state.map.dimensions.height - ? -scaledMapSize + state.map.dimensions.height - : y - - dispatch(setMapPosition(updatedX, updatedY)) - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/index.js b/opendc-web/opendc-web-ui/src/redux/index.js index 3c7ad55f..fa0c9d23 100644 --- a/opendc-web/opendc-web-ui/src/redux/index.js +++ b/opendc-web/opendc-web-ui/src/redux/index.js @@ -5,7 +5,6 @@ import createSagaMiddleware from 'redux-saga' import thunk from 'redux-thunk' import rootReducer from './reducers' import rootSaga from './sagas' -import { viewportAdjustmentMiddleware } from './middleware/viewport-adjustment' import { createReduxEnhancer } from '@sentry/react' let store @@ -13,7 +12,7 @@ let store function initStore(initialState, ctx) { const sagaMiddleware = createSagaMiddleware({ context: ctx }) - const middlewares = [thunk, sagaMiddleware, viewportAdjustmentMiddleware] + const middlewares = [thunk, sagaMiddleware] if (process.env.NODE_ENV !== 'production') { middlewares.push(createLogger()) 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 deleted file mode 100644 index c2fc5004..00000000 --- a/opendc-web/opendc-web-ui/src/redux/middleware/viewport-adjustment.js +++ /dev/null @@ -1,73 +0,0 @@ -import { SET_MAP_DIMENSIONS, setMapPosition, setMapScale } from '../actions/map' -import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building' -import { - MAP_MAX_SCALE, - MAP_MIN_SCALE, - SIDEBAR_WIDTH, - TILE_SIZE_IN_PIXELS, - VIEWPORT_PADDING, -} from '../../components/app/map/MapConstants' -import { calculateRoomListBounds } from '../../util/tile-calculations' - -export const viewportAdjustmentMiddleware = (store) => (next) => (action) => { - const state = store.getState() - - let topologyId = '-1' - let mapDimensions = {} - if (action.type === SET_CURRENT_TOPOLOGY && action.topologyId !== '-1') { - topologyId = action.topologyId - mapDimensions = state.map.dimensions - } else if (action.type === SET_MAP_DIMENSIONS && state.currentTopologyId !== '-1') { - topologyId = state.currentTopologyId - mapDimensions = { width: action.width, height: action.height } - } - - 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.tiles.map((tileId) => state.objects.tile[tileId]))) - - let hasNoTiles = true - for (let i in rooms) { - if (rooms[i].tiles.length > 0) { - hasNoTiles = false - break - } - } - - if (!hasNoTiles) { - const viewportParams = calculateParametersToZoomInOnRooms(rooms, mapDimensions.width, mapDimensions.height) - store.dispatch(setMapPosition(viewportParams.newX, viewportParams.newY)) - store.dispatch(setMapScale(viewportParams.newScale)) - } - } - - next(action) -} - -function calculateParametersToZoomInOnRooms(rooms, mapWidth, mapHeight) { - const bounds = calculateRoomListBounds(rooms) - const newScale = calculateNewScale(bounds, mapWidth, mapHeight) - - // Coordinates of the center of the room, relative to the global origin of the map - const roomCenterCoordinates = { - x: bounds.center.x * TILE_SIZE_IN_PIXELS * newScale, - y: bounds.center.y * TILE_SIZE_IN_PIXELS * newScale, - } - - const newX = -roomCenterCoordinates.x + mapWidth / 2 - const newY = -roomCenterCoordinates.y + mapHeight / 2 - - return { newScale, newX, newY } -} - -function calculateNewScale(bounds, mapWidth, mapHeight) { - const width = bounds.max.x - bounds.min.x - const height = bounds.max.y - bounds.min.y - - const scaleX = (mapWidth - 2 * SIDEBAR_WIDTH) / (width * TILE_SIZE_IN_PIXELS + 2 * VIEWPORT_PADDING) - const scaleY = mapHeight / (height * TILE_SIZE_IN_PIXELS + 2 * VIEWPORT_PADDING) - const newScale = Math.min(scaleX, scaleY) - - return Math.min(Math.max(MAP_MIN_SCALE, newScale), MAP_MAX_SCALE) -} 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 1b17a206..2f1359d6 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/index.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/index.js @@ -2,13 +2,11 @@ import { combineReducers } from 'redux' import { construction } from './construction-mode' import { currentTopologyId } from './current-ids' import { interactionLevel } from './interaction-level' -import { map } from './map' import { objects } from './objects' const rootReducer = combineReducers({ objects, construction, - map, currentTopologyId, interactionLevel, }) diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/map.js b/opendc-web/opendc-web-ui/src/redux/reducers/map.js deleted file mode 100644 index de712c15..00000000 --- a/opendc-web/opendc-web-ui/src/redux/reducers/map.js +++ /dev/null @@ -1,35 +0,0 @@ -import { combineReducers } from 'redux' -import { SET_MAP_DIMENSIONS, SET_MAP_POSITION, SET_MAP_SCALE } from '../actions/map' - -export function position(state = { x: 0, y: 0 }, action) { - switch (action.type) { - case SET_MAP_POSITION: - return { x: action.x, y: action.y } - default: - return state - } -} - -export function dimensions(state = { width: 600, height: 400 }, action) { - switch (action.type) { - case SET_MAP_DIMENSIONS: - return { width: action.width, height: action.height } - default: - return state - } -} - -export function scale(state = 1, action) { - switch (action.type) { - case SET_MAP_SCALE: - return action.scale - default: - return state - } -} - -export const map = combineReducers({ - position, - dimensions, - scale, -}) -- cgit v1.2.3 From 54d07120191eb81de91a49cdebf619cfecce2666 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 19 Jul 2021 14:45:25 +0200 Subject: refactor(ui): Encode state in topology actions This change updates the OpenDC frontend to reduce its reliance of global state during the execution of actions. Actions that modify the topology now require parameters to be passed via the action constructor instead of relying on the global interactionLevel state. --- .../opendc-web-ui/src/redux/actions/prefabs.js | 3 +- .../src/redux/actions/topology/building.js | 11 ++-- .../src/redux/actions/topology/machine.js | 10 +++- .../src/redux/actions/topology/rack.js | 9 ++- .../src/redux/actions/topology/room.js | 6 +- .../opendc-web-ui/src/redux/sagas/prefabs.js | 7 +-- .../opendc-web-ui/src/redux/sagas/topology.js | 67 ++++++++-------------- 7 files changed, 51 insertions(+), 62 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js b/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js index c112feed..0ef7795f 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js @@ -3,10 +3,11 @@ export const DELETE_PREFAB = 'DELETE_PREFAB' export const DELETE_PREFAB_SUCCEEDED = 'DELETE_PREFAB_SUCCEEDED' export const OPEN_PREFAB_SUCCEEDED = 'OPEN_PREFAB_SUCCEEDED' -export function addPrefab(name) { +export function addPrefab(name, tileId) { return { type: ADD_PREFAB, name, + tileId, } } 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 f1a7d569..49425318 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 @@ -55,13 +55,10 @@ export function cancelNewRoomConstructionSucceeded() { } } -export function startRoomEdit() { - return (dispatch, getState) => { - const { interactionLevel } = getState() - dispatch({ - type: START_ROOM_EDIT, - roomId: interactionLevel.roomId, - }) +export function startRoomEdit(roomId) { + return { + type: START_ROOM_EDIT, + roomId: roomId, } } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js index 17ccce5d..170b7648 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js @@ -2,23 +2,27 @@ export const DELETE_MACHINE = 'DELETE_MACHINE' export const ADD_UNIT = 'ADD_UNIT' export const DELETE_UNIT = 'DELETE_UNIT' -export function deleteMachine() { +export function deleteMachine(rackId, position) { return { type: DELETE_MACHINE, + rackId, + position, } } -export function addUnit(unitType, id) { +export function addUnit(machineId, unitType, id) { return { type: ADD_UNIT, + machineId, unitType, id, } } -export function deleteUnit(unitType, index) { +export function deleteUnit(machineId, unitType, index) { return { type: DELETE_UNIT, + machineId, unitType, index, } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js index b117402e..228e3ae9 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js @@ -2,22 +2,25 @@ export const EDIT_RACK_NAME = 'EDIT_RACK_NAME' export const DELETE_RACK = 'DELETE_RACK' export const ADD_MACHINE = 'ADD_MACHINE' -export function editRackName(name) { +export function editRackName(rackId, name) { return { type: EDIT_RACK_NAME, name, + rackId, } } -export function deleteRack() { +export function deleteRack(tileId) { return { type: DELETE_RACK, + tileId, } } -export function addMachine(position) { +export function addMachine(rackId, position) { return { type: ADD_MACHINE, position, + rackId, } } 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 80ef7c5e..e584af89 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 @@ -6,10 +6,11 @@ export const START_RACK_CONSTRUCTION = 'START_RACK_CONSTRUCTION' export const STOP_RACK_CONSTRUCTION = 'STOP_RACK_CONSTRUCTION' export const ADD_RACK_TO_TILE = 'ADD_RACK_TO_TILE' -export function editRoomName(name) { +export function editRoomName(roomId, name) { return { type: EDIT_ROOM_NAME, name, + roomId, } } @@ -41,8 +42,9 @@ export function addRackToTile(positionX, positionY) { } } -export function deleteRoom() { +export function deleteRoom(roomId) { return { type: DELETE_ROOM, + roomId, } } 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 91b03bf6..f717d878 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js @@ -4,14 +4,13 @@ import { addPrefab } from '../../api/prefabs' import { Rack } from '../../util/topology-schema' import { denormalize } from 'normalizr' -export function* onAddPrefab(action) { +export function* onAddPrefab({ name, tileId }) { try { - const interactionLevel = yield select((state) => state.interactionLevel) const objects = yield select((state) => state.objects) - const rack = objects.rack[objects.tile[interactionLevel.tileId].rack] + const rack = objects.rack[objects.tile[tileId].rack] const prefabRack = denormalize(rack, Rack, objects) const auth = yield getContext('auth') - const prefab = yield call(() => addPrefab(auth, { name: action.name, rack: prefabRack })) + const prefab = yield call(() => addPrefab(auth, { name, rack: prefabRack })) yield put(addToStore('prefab', prefab)) } 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 333c1485..f3742b78 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -37,10 +37,8 @@ export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = fa } } -export function* onAddTopology(action) { +export function* onAddTopology({ projectId, duplicateId, name }) { try { - const { projectId, duplicateId, name } = action - let topologyToBeCreated if (duplicateId) { topologyToBeCreated = yield denormalizeTopology(duplicateId) @@ -119,21 +117,19 @@ export function* onDeleteTile(action) { } } -export function* onEditRoomName(action) { +export function* onEditRoomName({ roomId, name }) { try { const topologyId = yield select((state) => state.currentTopologyId) - const roomId = yield select((state) => state.interactionLevel.roomId) - yield put(addPropToStoreObject('room', roomId, { name: action.name })) + yield put(addPropToStoreObject('room', roomId, { name })) yield updateTopologyOnServer(topologyId) } catch (error) { console.error(error) } } -export function* onDeleteRoom() { +export function* onDeleteRoom({ roomId }) { try { const topologyId = yield select((state) => state.currentTopologyId) - const roomId = yield select((state) => state.interactionLevel.roomId) yield put(goDownOneInteractionLevel()) yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'rooms', roomId)) yield updateTopologyOnServer(topologyId) @@ -142,21 +138,19 @@ export function* onDeleteRoom() { } } -export function* onEditRackName(action) { +export function* onEditRackName({ rackId, name }) { try { const topologyId = yield select((state) => state.currentTopologyId) - const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rack) - yield put(addPropToStoreObject('rack', rackId, { name: action.name })) + yield put(addPropToStoreObject('rack', rackId, { name })) yield updateTopologyOnServer(topologyId) } catch (error) { console.error(error) } } -export function* onDeleteRack() { +export function* onDeleteRack({ tileId }) { try { const topologyId = yield select((state) => state.currentTopologyId) - const tileId = yield select((state) => state.interactionLevel.tileId) yield put(goDownOneInteractionLevel()) yield put(addPropToStoreObject('tile', tileId, { rack: undefined })) yield updateTopologyOnServer(topologyId) @@ -165,35 +159,34 @@ export function* onDeleteRack() { } } -export function* onAddRackToTile(action) { +export function* onAddRackToTile({ tileId }) { try { const topologyId = yield select((state) => state.currentTopologyId) const rack = { _id: uuid(), name: 'Rack', - tileId: action.tileId, + tileId, capacity: DEFAULT_RACK_SLOT_CAPACITY, powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, machines: [], } yield put(addToStore('rack', rack)) - yield put(addPropToStoreObject('tile', action.tileId, { rack: rack._id })) + yield put(addPropToStoreObject('tile', tileId, { rack: rack._id })) yield updateTopologyOnServer(topologyId) } catch (error) { console.error(error) } } -export function* onAddMachine(action) { +export function* onAddMachine({ rackId, position }) { try { const topologyId = yield select((state) => state.currentTopologyId) - 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, + position, cpus: [], gpus: [], memories: [], @@ -209,15 +202,13 @@ export function* onAddMachine(action) { } } -export function* onDeleteMachine() { +export function* onDeleteMachine({ rackId, position }) { 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].rack]) + const rack = yield select((state) => state.objects.rack[rackId]) yield put(goDownOneInteractionLevel()) yield put( - addPropToStoreObject('rack', rack._id, { machines: rack.machines.filter((_, idx) => idx !== position - 1) }) + addPropToStoreObject('rack', rackId, { machines: rack.machines.filter((_, idx) => idx !== position - 1) }) ) yield updateTopologyOnServer(topologyId) } catch (error) { @@ -232,23 +223,19 @@ const unitMapping = { storage: 'storages', } -export function* onAddUnit(action) { +export function* onAddUnit({ machineId, unitType, id }) { 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].rack].machines[position - 1]] - ) + const machine = yield select((state) => state.objects.machine[machineId]) - if (machine[unitMapping[action.unitType]].length >= MAX_NUM_UNITS_PER_MACHINE) { + if (machine[unitMapping[unitType]].length >= MAX_NUM_UNITS_PER_MACHINE) { return } - const units = [...machine[unitMapping[action.unitType]], action.id] + const units = [...machine[unitMapping[unitType]], id] yield put( addPropToStoreObject('machine', machine._id, { - [unitMapping[action.unitType]]: units, + [unitMapping[unitType]]: units, }) ) yield updateTopologyOnServer(topologyId) @@ -257,20 +244,16 @@ export function* onAddUnit(action) { } } -export function* onDeleteUnit(action) { +export function* onDeleteUnit({ machineId, unitType, index }) { 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].rack].machines[position - 1]] - ) - const unitIds = machine[unitMapping[action.unitType]].slice() - unitIds.splice(action.index, 1) + const machine = yield select((state) => state.objects.machine[machineId]) + const unitIds = machine[unitMapping[unitType]].slice() + unitIds.splice(index, 1) yield put( addPropToStoreObject('machine', machine._id, { - [unitMapping[action.unitType]]: unitIds, + [unitMapping[unitType]]: unitIds, }) ) yield updateTopologyOnServer(topologyId) -- cgit v1.2.3 From 5e5ab63b280eb446db4090733cd3ad2e97d02018 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 19 Jul 2021 15:47:23 +0200 Subject: refactor(ui): Restructure components per page This change updates the source structure of the OpenDC frontend to follow the page structure. --- opendc-web/opendc-web-ui/src/redux/sagas/topology.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/redux') 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 f3742b78..a5a3be32 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -15,7 +15,7 @@ import { DEFAULT_RACK_POWER_CAPACITY, DEFAULT_RACK_SLOT_CAPACITY, MAX_NUM_UNITS_PER_MACHINE, -} from '../../components/app/map/MapConstants' +} from '../../components/topologies/map/MapConstants' import { fetchAndStoreTopology, denormalizeTopology, updateTopologyOnServer } from './objects' import { uuid } from 'uuidv4' import { addTopology } from '../../api/topologies' @@ -45,7 +45,7 @@ export function* onAddTopology({ projectId, duplicateId, name }) { topologyToBeCreated = { ...topologyToBeCreated, name } delete topologyToBeCreated._id } else { - topologyToBeCreated = { name: action.name, rooms: [] } + topologyToBeCreated = { name, rooms: [] } } const auth = yield getContext('auth') -- cgit v1.2.3 From 6e3ad713111f35fc58bd2b7f1be5aeeb57eb94a8 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 20 Jul 2021 14:09:39 +0200 Subject: refactor(ui): Perform Saga mutations through React Query This change updates the OpenDC frontend to perform mutations of the topology done in Sagas through the React Query cache, so that non-Saga parts of the application also have their topology queries updated. --- .../opendc-web-ui/src/redux/sagas/objects.js | 36 ---------------- opendc-web/opendc-web-ui/src/redux/sagas/query.js | 41 ++++++++++++++++++ .../opendc-web-ui/src/redux/sagas/topology.js | 48 ++++++++++++++++++---- 3 files changed, 82 insertions(+), 43 deletions(-) delete mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/objects.js create mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/query.js (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js deleted file mode 100644 index 9b4f8094..00000000 --- a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js +++ /dev/null @@ -1,36 +0,0 @@ -import { call, put, select, getContext } from 'redux-saga/effects' -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 auth = yield getContext('auth') - - let topology = yield select((state) => state.objects.topology[id]) - if (!topology) { - const newTopology = yield call(fetchTopology, auth, id) - const { entities } = normalize(newTopology, Topology) - yield put(storeTopology(entities)) - } - - return topology -} - -export const updateTopologyOnServer = function* (id) { - const topology = yield denormalizeTopology(id) - const auth = yield getContext('auth') - yield call(updateTopology, auth, topology) -} - -/** - * 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/query.js b/opendc-web/opendc-web-ui/src/redux/sagas/query.js new file mode 100644 index 00000000..787006c7 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/sagas/query.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { MutationObserver } from 'react-query' +import { getContext, call } from 'redux-saga/effects' + +/** + * Fetch the query with the specified key. + */ +export function* fetchQuery(key, options) { + const queryClient = yield getContext('queryClient') + return yield call([queryClient, queryClient.fetchQuery], key, options) +} + +/** + * Perform a mutation with the specified key. + */ +export function* mutate(key, object, options) { + const queryClient = yield getContext('queryClient') + const mutationObserver = new MutationObserver(queryClient, { mutationKey: key }) + return yield call([mutationObserver, mutationObserver.mutate], object, options) +} 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 a5a3be32..2d61643b 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -1,4 +1,6 @@ -import { call, put, select, getContext } from 'redux-saga/effects' +import { normalize, denormalize } from 'normalizr' +import { put, select } from 'redux-saga/effects' +import { Topology } from '../../util/topology-schema' import { goDownOneInteractionLevel } from '../actions/interaction-level' import { addIdToStoreObjectListProp, @@ -6,6 +8,7 @@ import { addToStore, removeIdFromStoreObjectListProp, } from '../actions/objects' +import { storeTopology } from '../actions/topologies' import { cancelNewRoomConstructionSucceeded, setCurrentTopology, @@ -16,14 +19,15 @@ import { DEFAULT_RACK_SLOT_CAPACITY, MAX_NUM_UNITS_PER_MACHINE, } from '../../components/topologies/map/MapConstants' -import { fetchAndStoreTopology, denormalizeTopology, updateTopologyOnServer } from './objects' import { uuid } from 'uuidv4' -import { addTopology } from '../../api/topologies' +import { fetchQuery, mutate } from './query' +/** + * Fetches all topologies of the project with the specified identifier. + */ export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = false) { try { - const queryClient = yield getContext('queryClient') - const project = yield call(() => queryClient.fetchQuery(['projects', projectId])) + const project = yield fetchQuery(['projects', projectId]) for (const id of project.topologyIds) { yield fetchAndStoreTopology(id) @@ -37,6 +41,37 @@ export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = fa } } +/** + * Fetches and normalizes the topology with the specified identifier. + */ +export function* fetchAndStoreTopology(id) { + let topology = yield select((state) => state.objects.topology[id]) + if (!topology) { + const newTopology = yield fetchQuery(['topologies', id]) + const { entities } = normalize(newTopology, Topology) + yield put(storeTopology(entities)) + } + + return topology +} + +/** + * Synchronize the topology with the specified identifier with the server. + */ +export function* updateTopologyOnServer(id) { + const topology = yield denormalizeTopology(id) + yield mutate('updateTopology', topology) +} + +/** + * Denormalizes the topology representation in order to be stored on the server. + */ +export function* denormalizeTopology(id) { + const objects = yield select((state) => state.objects) + const topology = objects.topology[id] + return denormalize(topology, Topology, objects) +} + export function* onAddTopology({ projectId, duplicateId, name }) { try { let topologyToBeCreated @@ -48,8 +83,7 @@ export function* onAddTopology({ projectId, duplicateId, name }) { topologyToBeCreated = { name, rooms: [] } } - const auth = yield getContext('auth') - const topology = yield call(addTopology, auth, { ...topologyToBeCreated, projectId }) + const topology = yield mutate('addTopology', { ...topologyToBeCreated, projectId }) yield fetchAndStoreTopology(topology._id) yield put(setCurrentTopology(topology._id)) } catch (error) { -- cgit v1.2.3 From ebab0cc12e293a57cbc58d2dd51b3c9d7cd4ee92 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 20 Jul 2021 14:55:39 +0200 Subject: fix(ui): Load correct topology view This change fixes an issue where the only the default topology view would be shown to the user. --- .../opendc-web-ui/src/redux/actions/projects.js | 8 ----- .../opendc-web-ui/src/redux/actions/topologies.js | 8 +++++ opendc-web/opendc-web-ui/src/redux/sagas/index.js | 7 ++--- .../opendc-web-ui/src/redux/sagas/projects.js | 9 ------ .../opendc-web-ui/src/redux/sagas/topology.js | 34 ++++++++-------------- 5 files changed, 23 insertions(+), 43 deletions(-) delete mode 100644 opendc-web/opendc-web-ui/src/redux/actions/projects.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/projects.js (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/actions/projects.js b/opendc-web/opendc-web-ui/src/redux/actions/projects.js deleted file mode 100644 index 4fe6f6a8..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/projects.js +++ /dev/null @@ -1,8 +0,0 @@ -export const OPEN_PROJECT_SUCCEEDED = 'OPEN_PROJECT_SUCCEEDED' - -export function openProjectSucceeded(id) { - return { - type: OPEN_PROJECT_SUCCEEDED, - id, - } -} 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 529e8663..4888c4da 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topologies.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topologies.js @@ -1,6 +1,14 @@ +export const OPEN_TOPOLOGY = 'OPEN_TOPOLOGY' export const ADD_TOPOLOGY = 'ADD_TOPOLOGY' export const STORE_TOPOLOGY = 'STORE_TOPOLOGY' +export function openTopology(id) { + return { + type: OPEN_TOPOLOGY, + id, + } +} + export function addTopology(projectId, name, duplicateId) { return { type: ADD_TOPOLOGY, 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 318f0afb..9ddc564d 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/index.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/index.js @@ -1,5 +1,4 @@ import { takeEvery } from 'redux-saga/effects' -import { OPEN_PROJECT_SUCCEEDED } from '../actions/projects' import { ADD_TILE, CANCEL_NEW_ROOM_CONSTRUCTION, @@ -9,7 +8,6 @@ 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 { onOpenProjectSucceeded } from './projects' import { onAddMachine, onAddRackToTile, @@ -25,13 +23,14 @@ import { onEditRackName, onEditRoomName, onStartNewRoomConstruction, + onOpenTopology, } from './topology' -import { ADD_TOPOLOGY } from '../actions/topologies' +import { ADD_TOPOLOGY, OPEN_TOPOLOGY } from '../actions/topologies' import { onAddPrefab } from './prefabs' import { ADD_PREFAB } from '../actions/prefabs' export default function* rootSaga() { - yield takeEvery(OPEN_PROJECT_SUCCEEDED, onOpenProjectSucceeded) + yield takeEvery(OPEN_TOPOLOGY, onOpenTopology) yield takeEvery(ADD_TOPOLOGY, onAddTopology) yield takeEvery(START_NEW_ROOM_CONSTRUCTION, onStartNewRoomConstruction) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/projects.js b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js deleted file mode 100644 index 5809d4d2..00000000 --- a/opendc-web/opendc-web-ui/src/redux/sagas/projects.js +++ /dev/null @@ -1,9 +0,0 @@ -import { fetchAndStoreAllTopologiesOfProject } from './topology' - -export function* onOpenProjectSucceeded(action) { - try { - yield fetchAndStoreAllTopologiesOfProject(action.id, true) - } 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 2d61643b..fb6f7f0d 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -22,29 +22,10 @@ import { import { uuid } from 'uuidv4' import { fetchQuery, mutate } from './query' -/** - * Fetches all topologies of the project with the specified identifier. - */ -export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = false) { - try { - const project = yield fetchQuery(['projects', projectId]) - - for (const id of project.topologyIds) { - yield fetchAndStoreTopology(id) - } - - if (setTopology) { - yield put(setCurrentTopology(project.topologyIds[0])) - } - } catch (error) { - console.error(error) - } -} - /** * Fetches and normalizes the topology with the specified identifier. */ -export function* fetchAndStoreTopology(id) { +function* fetchAndStoreTopology(id) { let topology = yield select((state) => state.objects.topology[id]) if (!topology) { const newTopology = yield fetchQuery(['topologies', id]) @@ -58,7 +39,7 @@ export function* fetchAndStoreTopology(id) { /** * Synchronize the topology with the specified identifier with the server. */ -export function* updateTopologyOnServer(id) { +function* updateTopologyOnServer(id) { const topology = yield denormalizeTopology(id) yield mutate('updateTopology', topology) } @@ -66,12 +47,21 @@ export function* updateTopologyOnServer(id) { /** * Denormalizes the topology representation in order to be stored on the server. */ -export function* denormalizeTopology(id) { +function* denormalizeTopology(id) { const objects = yield select((state) => state.objects) const topology = objects.topology[id] return denormalize(topology, Topology, objects) } +export function* onOpenTopology({ id }) { + try { + yield fetchAndStoreTopology(id) + yield put(setCurrentTopology(id)) + } catch (error) { + console.error(error) + } +} + export function* onAddTopology({ projectId, duplicateId, name }) { try { let topologyToBeCreated -- cgit v1.2.3 From 28a4259c43e6180723b15a8c36a9b36871420f8a Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 20 Jul 2021 14:59:58 +0200 Subject: feat(ui): Add table view for topology rooms --- opendc-web/opendc-web-ui/src/redux/actions/interaction-level.js | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/actions/interaction-level.js b/opendc-web/opendc-web-ui/src/redux/actions/interaction-level.js index ff6b1fa3..8381eeef 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/interaction-level.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/interaction-level.js @@ -3,6 +3,13 @@ export const GO_FROM_ROOM_TO_RACK = 'GO_FROM_ROOM_TO_RACK' export const GO_FROM_RACK_TO_MACHINE = 'GO_FROM_RACK_TO_MACHINE' export const GO_DOWN_ONE_INTERACTION_LEVEL = 'GO_DOWN_ONE_INTERACTION_LEVEL' +export function goToRoom(roomId) { + return { + type: GO_FROM_BUILDING_TO_ROOM, + roomId, + } +} + export function goFromBuildingToRoom(roomId) { return (dispatch, getState) => { const { interactionLevel } = getState() -- cgit v1.2.3 From 54f424a18cc21a52ea518d40893218a07ab55989 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 21 Jul 2021 15:04:22 +0200 Subject: feat(ui): Extract topology construction out of Sagas This change updates the OpenDC frontend to perform the construction of the topology directly in the reducers instead of performing the mutations in Redux Sagas as side effects. This allows us to nicely map actions to mutations in the reducers. --- .../opendc-web-ui/src/redux/actions/objects.js | 41 --- .../opendc-web-ui/src/redux/actions/prefabs.js | 33 --- .../opendc-web-ui/src/redux/actions/topologies.js | 3 +- .../src/redux/actions/topology/building.js | 56 ++-- .../src/redux/actions/topology/machine.js | 13 +- .../src/redux/actions/topology/rack.js | 16 +- .../src/redux/actions/topology/room.js | 32 +- .../src/redux/reducers/construction-mode.js | 3 - .../src/redux/reducers/current-ids.js | 10 - .../opendc-web-ui/src/redux/reducers/index.js | 6 +- .../src/redux/reducers/interaction-level.js | 23 +- .../opendc-web-ui/src/redux/reducers/objects.js | 56 ---- .../src/redux/reducers/topology/index.js | 44 +++ .../src/redux/reducers/topology/machine.js | 47 +++ .../src/redux/reducers/topology/rack.js | 66 +++++ .../src/redux/reducers/topology/room.js | 65 +++++ .../src/redux/reducers/topology/tile.js | 59 ++++ .../src/redux/reducers/topology/topology.js | 47 +++ opendc-web/opendc-web-ui/src/redux/sagas/index.js | 52 +--- .../opendc-web-ui/src/redux/sagas/prefabs.js | 18 -- opendc-web/opendc-web-ui/src/redux/sagas/query.js | 41 --- .../opendc-web-ui/src/redux/sagas/topology.js | 324 ++++----------------- 22 files changed, 492 insertions(+), 563 deletions(-) delete mode 100644 opendc-web/opendc-web-ui/src/redux/actions/objects.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/actions/prefabs.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/objects.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/topology/index.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js create mode 100644 opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js delete mode 100644 opendc-web/opendc-web-ui/src/redux/sagas/query.js (limited to 'opendc-web/opendc-web-ui/src/redux') diff --git a/opendc-web/opendc-web-ui/src/redux/actions/objects.js b/opendc-web/opendc-web-ui/src/redux/actions/objects.js deleted file mode 100644 index 7b648b18..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/objects.js +++ /dev/null @@ -1,41 +0,0 @@ -export const ADD_TO_STORE = 'ADD_TO_STORE' -export const ADD_PROP_TO_STORE_OBJECT = 'ADD_PROP_TO_STORE_OBJECT' -export const ADD_ID_TO_STORE_OBJECT_LIST_PROP = 'ADD_ID_TO_STORE_OBJECT_LIST_PROP' -export const REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP = 'REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP' - -export function addToStore(objectType, object) { - return { - type: ADD_TO_STORE, - objectType, - object, - } -} - -export function addPropToStoreObject(objectType, objectId, propObject) { - return { - type: ADD_PROP_TO_STORE_OBJECT, - objectType, - objectId, - propObject, - } -} - -export function addIdToStoreObjectListProp(objectType, objectId, propName, id) { - return { - type: ADD_ID_TO_STORE_OBJECT_LIST_PROP, - objectType, - objectId, - propName, - id, - } -} - -export function removeIdFromStoreObjectListProp(objectType, objectId, propName, id) { - return { - type: REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP, - objectType, - objectId, - propName, - id, - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js b/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js deleted file mode 100644 index 0ef7795f..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js +++ /dev/null @@ -1,33 +0,0 @@ -export const ADD_PREFAB = 'ADD_PREFAB' -export const DELETE_PREFAB = 'DELETE_PREFAB' -export const DELETE_PREFAB_SUCCEEDED = 'DELETE_PREFAB_SUCCEEDED' -export const OPEN_PREFAB_SUCCEEDED = 'OPEN_PREFAB_SUCCEEDED' - -export function addPrefab(name, tileId) { - return { - type: ADD_PREFAB, - name, - tileId, - } -} - -export function deletePrefab(id) { - return { - type: DELETE_PREFAB, - id, - } -} - -export function deletePrefabSucceeded(id) { - return { - type: DELETE_PREFAB_SUCCEEDED, - id, - } -} - -export function openPrefabSucceeded(id) { - return { - type: OPEN_PREFAB_SUCCEEDED, - id, - } -} 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 4888c4da..fc697cc2 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topologies.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topologies.js @@ -18,9 +18,10 @@ export function addTopology(projectId, name, duplicateId) { } } -export function storeTopology(entities) { +export function storeTopology(topology, entities) { return { type: STORE_TOPOLOGY, + 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 49425318..939c24a4 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 @@ -1,4 +1,6 @@ -export const SET_CURRENT_TOPOLOGY = 'SET_CURRENT_TOPOLOGY' +import { uuid } from 'uuidv4' +import { addRoom, deleteRoom } from './room' + export const START_NEW_ROOM_CONSTRUCTION = 'START_NEW_ROOM_CONSTRUCTION' export const START_NEW_ROOM_CONSTRUCTION_SUCCEEDED = 'START_NEW_ROOM_CONSTRUCTION_SUCCEEDED' export const FINISH_NEW_ROOM_CONSTRUCTION = 'FINISH_NEW_ROOM_CONSTRUCTION' @@ -9,16 +11,19 @@ export const FINISH_ROOM_EDIT = 'FINISH_ROOM_EDIT' export const ADD_TILE = 'ADD_TILE' export const DELETE_TILE = 'DELETE_TILE' -export function setCurrentTopology(topologyId) { - return { - type: SET_CURRENT_TOPOLOGY, - topologyId, - } -} - export function startNewRoomConstruction() { - return { - type: START_NEW_ROOM_CONSTRUCTION, + return (dispatch, getState) => { + const { topology } = getState() + const topologyId = topology.root._id + const room = { + _id: uuid(), + name: 'Room', + topologyId, + tiles: [], + } + + dispatch(addRoom(topologyId, room)) + dispatch(startNewRoomConstructionSucceeded(room._id)) } } @@ -31,8 +36,8 @@ export function startNewRoomConstructionSucceeded(roomId) { export function finishNewRoomConstruction() { return (dispatch, getState) => { - const { objects, construction } = getState() - if (objects.room[construction.currentRoomInConstruction].tiles.length === 0) { + const { topology, construction } = getState() + if (topology.rooms[construction.currentRoomInConstruction].tiles.length === 0) { dispatch(cancelNewRoomConstruction()) return } @@ -44,8 +49,11 @@ export function finishNewRoomConstruction() { } export function cancelNewRoomConstruction() { - return { - type: CANCEL_NEW_ROOM_CONSTRUCTION, + return (dispatch, getState) => { + const { construction } = getState() + const roomId = construction.currentRoomInConstruction + dispatch(deleteRoom(roomId)) + dispatch(cancelNewRoomConstructionSucceeded()) } } @@ -70,24 +78,30 @@ export function finishRoomEdit() { export function toggleTileAtLocation(positionX, positionY) { return (dispatch, getState) => { - const { objects, construction } = getState() + const { topology, construction } = getState() - const tileIds = objects.room[construction.currentRoomInConstruction].tiles + const roomId = construction.currentRoomInConstruction + const tileIds = topology.rooms[roomId].tiles for (const tileId of tileIds) { - if (objects.tile[tileId].positionX === positionX && objects.tile[tileId].positionY === positionY) { + if (topology.tiles[tileId].positionX === positionX && topology.tiles[tileId].positionY === positionY) { dispatch(deleteTile(tileId)) return } } - dispatch(addTile(positionX, positionY)) + + dispatch(addTile(roomId, positionX, positionY)) } } -export function addTile(positionX, positionY) { +export function addTile(roomId, positionX, positionY) { return { type: ADD_TILE, - positionX, - positionY, + tile: { + _id: uuid(), + roomId, + positionX, + positionY, + }, } } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js index 170b7648..93320884 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js @@ -2,28 +2,27 @@ export const DELETE_MACHINE = 'DELETE_MACHINE' export const ADD_UNIT = 'ADD_UNIT' export const DELETE_UNIT = 'DELETE_UNIT' -export function deleteMachine(rackId, position) { +export function deleteMachine(machineId) { return { type: DELETE_MACHINE, - rackId, - position, + machineId, } } -export function addUnit(machineId, unitType, id) { +export function addUnit(machineId, unitType, unitId) { return { type: ADD_UNIT, machineId, unitType, - id, + unitId, } } -export function deleteUnit(machineId, unitType, index) { +export function deleteUnit(machineId, unitType, unitId) { return { type: DELETE_UNIT, machineId, unitType, - index, + unitId, } } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js index 228e3ae9..c319d966 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js @@ -1,3 +1,5 @@ +import { uuid } from 'uuidv4' + export const EDIT_RACK_NAME = 'EDIT_RACK_NAME' export const DELETE_RACK = 'DELETE_RACK' export const ADD_MACHINE = 'ADD_MACHINE' @@ -10,9 +12,10 @@ export function editRackName(rackId, name) { } } -export function deleteRack(tileId) { +export function deleteRack(tileId, rackId) { return { type: DELETE_RACK, + rackId, tileId, } } @@ -20,7 +23,14 @@ export function deleteRack(tileId) { export function addMachine(rackId, position) { return { type: ADD_MACHINE, - position, - rackId, + machine: { + _id: uuid(), + rackId, + position, + cpus: [], + gpus: [], + memories: [], + storages: [], + }, } } 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 e584af89..bd447db5 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 @@ -1,11 +1,28 @@ +import { uuid } from 'uuidv4' +import { + DEFAULT_RACK_SLOT_CAPACITY, + DEFAULT_RACK_POWER_CAPACITY, +} from '../../../components/topologies/map/MapConstants' import { findTileWithPosition } from '../../../util/tile-calculations' +export const ADD_ROOM = 'ADD_ROOM' export const EDIT_ROOM_NAME = 'EDIT_ROOM_NAME' export const DELETE_ROOM = 'DELETE_ROOM' export const START_RACK_CONSTRUCTION = 'START_RACK_CONSTRUCTION' export const STOP_RACK_CONSTRUCTION = 'STOP_RACK_CONSTRUCTION' export const ADD_RACK_TO_TILE = 'ADD_RACK_TO_TILE' +export function addRoom(topologyId, room) { + return { + type: ADD_ROOM, + room: { + _id: uuid(), + topologyId, + ...room, + }, + } +} + export function editRoomName(roomId, name) { return { type: EDIT_ROOM_NAME, @@ -28,15 +45,22 @@ export function stopRackConstruction() { export function addRackToTile(positionX, positionY) { return (dispatch, getState) => { - const { objects, interactionLevel } = getState() - const currentRoom = objects.room[interactionLevel.roomId] - const tiles = currentRoom.tiles.map((tileId) => objects.tile[tileId]) + const { topology, interactionLevel } = getState() + const currentRoom = topology.rooms[interactionLevel.roomId] + const tiles = currentRoom.tiles.map((tileId) => topology.tiles[tileId]) const tile = findTileWithPosition(tiles, positionX, positionY) if (tile !== null) { dispatch({ type: ADD_RACK_TO_TILE, - tileId: tile._id, + rack: { + _id: uuid(), + name: 'Rack', + tileId: tile._id, + capacity: DEFAULT_RACK_SLOT_CAPACITY, + powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, + machines: [], + }, }) } } 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 5bac7fea..d0aac5ae 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 @@ -4,7 +4,6 @@ import { CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED, FINISH_NEW_ROOM_CONSTRUCTION, FINISH_ROOM_EDIT, - SET_CURRENT_TOPOLOGY, START_NEW_ROOM_CONSTRUCTION_SUCCEEDED, START_ROOM_EDIT, } from '../actions/topology/building' @@ -19,7 +18,6 @@ export function currentRoomInConstruction(state = '-1', action) { case CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED: case FINISH_NEW_ROOM_CONSTRUCTION: case FINISH_ROOM_EDIT: - case SET_CURRENT_TOPOLOGY: case DELETE_ROOM: return '-1' default: @@ -32,7 +30,6 @@ export function inRackConstructionMode(state = false, action) { case START_RACK_CONSTRUCTION: return true case STOP_RACK_CONSTRUCTION: - case SET_CURRENT_TOPOLOGY: case GO_DOWN_ONE_INTERACTION_LEVEL: return false default: 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 deleted file mode 100644 index c0baf567..00000000 --- a/opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js +++ /dev/null @@ -1,10 +0,0 @@ -import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building' - -export function currentTopologyId(state = '-1', action) { - switch (action.type) { - case SET_CURRENT_TOPOLOGY: - return action.topologyId - 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 2f1359d6..7ffb1211 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/index.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/index.js @@ -1,13 +1,11 @@ import { combineReducers } from 'redux' import { construction } from './construction-mode' -import { currentTopologyId } from './current-ids' import { interactionLevel } from './interaction-level' -import { objects } from './objects' +import topology from './topology' const rootReducer = combineReducers({ - objects, + topology, construction, - currentTopologyId, 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 9f23949f..b30c68b9 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 @@ -4,14 +4,12 @@ import { GO_FROM_RACK_TO_MACHINE, GO_FROM_ROOM_TO_RACK, } from '../actions/interaction-level' -import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building' +import { DELETE_MACHINE } from '../actions/topology/machine' +import { DELETE_RACK } from '../actions/topology/rack' +import { DELETE_ROOM } from '../actions/topology/room' export function interactionLevel(state = { mode: 'BUILDING' }, action) { switch (action.type) { - case SET_CURRENT_TOPOLOGY: - return { - mode: 'BUILDING', - } case GO_FROM_BUILDING_TO_ROOM: return { mode: 'ROOM', @@ -49,6 +47,21 @@ export function interactionLevel(state = { mode: 'BUILDING' }, action) { } else { return state } + case DELETE_MACHINE: + return { + mode: 'RACK', + roomId: state.roomId, + tileId: state.tileId, + } + case DELETE_RACK: + return { + mode: 'ROOM', + roomId: state.roomId, + } + case DELETE_ROOM: + return { + mode: 'BUILDING', + } default: return state } diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/objects.js b/opendc-web/opendc-web-ui/src/redux/reducers/objects.js deleted file mode 100644 index 11f6d353..00000000 --- a/opendc-web/opendc-web-ui/src/redux/reducers/objects.js +++ /dev/null @@ -1,56 +0,0 @@ -import { combineReducers } from 'redux' -import { - ADD_ID_TO_STORE_OBJECT_LIST_PROP, - ADD_PROP_TO_STORE_OBJECT, - ADD_TO_STORE, - 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({ - cpu: object('cpu', CPU_UNITS), - gpu: object('gpu', GPU_UNITS), - memory: object('memory', MEMORY_UNITS), - storage: object('storage', STORAGE_UNITS), - machine: object('machine'), - rack: object('rack'), - tile: object('tile'), - room: object('room'), - topology: object('topology'), - prefab: object('prefab'), -}) - -function object(type, defaultState = {}) { - return objectWithId(type, (object) => object._id, defaultState) -} - -function objectWithId(type, getId, defaultState = {}) { - return (state = defaultState, action) => { - if (action.type === STORE_TOPOLOGY) { - return { ...state, ...action.entities[type] } - } else if (action.objectType !== type) { - return state - } - - if (action.type === ADD_TO_STORE) { - return { ...state, [getId(action.object)]: action.object } - } else if (action.type === ADD_PROP_TO_STORE_OBJECT) { - 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], { - [action.propName]: [...state[action.objectId][action.propName], action.id], - }), - }) - } else if (action.type === REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP) { - return Object.assign({}, state, { - [action.objectId]: Object.assign({}, state[action.objectId], { - [action.propName]: state[action.objectId][action.propName].filter((id) => id !== action.id), - }), - }) - } - - return state - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/index.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/index.js new file mode 100644 index 00000000..b1c7d29e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/index.js @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { CPU_UNITS, GPU_UNITS, MEMORY_UNITS, STORAGE_UNITS } from '../../../util/unit-specifications' +import machine from './machine' +import rack from './rack' +import room from './room' +import tile from './tile' +import topology from './topology' + +function objects(state = {}, action) { + return { + cpus: CPU_UNITS, + gpus: GPU_UNITS, + memories: MEMORY_UNITS, + storages: STORAGE_UNITS, + machines: machine(state.machines, action, state), + racks: rack(state.racks, action, state), + tiles: tile(state.tiles, action, state), + rooms: room(state.rooms, action, state), + root: topology(state.root, action, state), + } +} + +export default objects diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js new file mode 100644 index 00000000..41773014 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js @@ -0,0 +1,47 @@ +import produce from 'immer' +import { STORE_TOPOLOGY } from '../../actions/topologies' +import { DELETE_MACHINE, ADD_UNIT, DELETE_UNIT } from '../../actions/topology/machine' +import { ADD_MACHINE, DELETE_RACK } from '../../actions/topology/rack' + +function machine(state = {}, action, { racks }) { + switch (action.type) { + case STORE_TOPOLOGY: + return action.entities.machines || {} + case ADD_MACHINE: + return produce(state, (draft) => { + const { machine } = action + draft[machine._id] = machine + }) + case DELETE_MACHINE: + return produce(state, (draft) => { + const { machineId } = action + delete draft[machineId] + }) + case ADD_UNIT: + return produce(state, (draft) => { + const { machineId, unitType, unitId } = action + draft[machineId][unitType].push(unitId) + }) + case DELETE_UNIT: + return produce(state, (draft) => { + const { machineId, unitType, unitId } = action + const units = draft[machineId][unitType] + const index = units.indexOf(unitId) + units.splice(index, 1) + }) + case DELETE_RACK: + return produce(state, (draft) => { + const { rackId } = action + const rack = racks[rackId] + + for (const id of rack.machines) { + const machine = draft[id] + machine.rackId = undefined + } + }) + default: + return state + } +} + +export default machine diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js new file mode 100644 index 00000000..9cc37124 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js @@ -0,0 +1,66 @@ +/* + * 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 produce from 'immer' +import { STORE_TOPOLOGY } from '../../actions/topologies' +import { DELETE_MACHINE } from '../../actions/topology/machine' +import { DELETE_RACK, EDIT_RACK_NAME, ADD_MACHINE } from '../../actions/topology/rack' +import { ADD_RACK_TO_TILE } from '../../actions/topology/room' + +function rack(state = {}, action, { machines }) { + switch (action.type) { + case STORE_TOPOLOGY: + return action.entities.racks || {} + case ADD_RACK_TO_TILE: + return produce(state, (draft) => { + const { rack } = action + draft[rack._id] = rack + }) + case EDIT_RACK_NAME: + return produce(state, (draft) => { + const { rackId, name } = action + draft[rackId].name = name + }) + case DELETE_RACK: + return produce(state, (draft) => { + const { rackId } = action + delete draft[rackId] + }) + case ADD_MACHINE: + return produce(state, (draft) => { + const { machine } = action + draft[machine.rackId].machines.push(machine._id) + }) + case DELETE_MACHINE: + return produce(state, (draft) => { + const { machineId } = action + const machine = machines[machineId] + const rack = draft[machine.rackId] + const index = rack.machines.indexOf(machineId) + rack.machines.splice(index, 1) + }) + default: + return state + } +} + +export default rack diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js new file mode 100644 index 00000000..b61c9d82 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js @@ -0,0 +1,65 @@ +/* + * 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 produce from 'immer' +import { STORE_TOPOLOGY } from '../../actions/topologies' +import { ADD_TILE, DELETE_TILE } from '../../actions/topology/building' +import { DELETE_ROOM, EDIT_ROOM_NAME, ADD_ROOM } from '../../actions/topology/room' + +function room(state = {}, action, { tiles }) { + switch (action.type) { + case STORE_TOPOLOGY: + return action.entities.rooms || {} + case ADD_ROOM: + return produce(state, (draft) => { + const { room } = action + draft[room._id] = room + }) + case DELETE_ROOM: + return produce(state, (draft) => { + const { roomId } = action + delete draft[roomId] + }) + case EDIT_ROOM_NAME: + return produce(state, (draft) => { + const { roomId, name } = action + draft[roomId].name = name + }) + case ADD_TILE: + return produce(state, (draft) => { + const { tile } = action + draft[tile.roomId].tiles.push(tile._id) + }) + case DELETE_TILE: + return produce(state, (draft) => { + const { tileId } = action + const tile = tiles[tileId] + const room = draft[tile.roomId] + const index = room.tiles.indexOf(tileId) + room.tiles.splice(index, 1) + }) + default: + return state + } +} + +export default room diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js new file mode 100644 index 00000000..e0c5dd33 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js @@ -0,0 +1,59 @@ +/* + * 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 produce from 'immer' +import { STORE_TOPOLOGY } from '../../actions/topologies' +import { ADD_TILE, DELETE_TILE } from '../../actions/topology/building' +import { DELETE_RACK } from '../../actions/topology/rack' +import { ADD_RACK_TO_TILE } from '../../actions/topology/room' + +function tile(state = {}, action, { racks }) { + switch (action.type) { + case STORE_TOPOLOGY: + return action.entities.tiles || {} + case ADD_TILE: + return produce(state, (draft) => { + const { tile } = action + draft[tile._id] = tile + }) + case DELETE_TILE: + return produce(state, (draft) => { + const { tileId } = action + delete draft[tileId] + }) + case ADD_RACK_TO_TILE: + return produce(state, (draft) => { + const { rack } = action + draft[rack.tileId].rack = rack._id + }) + case DELETE_RACK: + return produce(state, (draft) => { + const { rackId } = action + const rack = racks[rackId] + draft[rack.tileId].rack = undefined + }) + default: + return state + } +} + +export default tile diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js new file mode 100644 index 00000000..da0e6988 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.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 produce from 'immer' +import { STORE_TOPOLOGY } from '../../actions/topologies' +import { ADD_ROOM, DELETE_ROOM } from '../../actions/topology/room' + +function topology(state = undefined, action) { + switch (action.type) { + case STORE_TOPOLOGY: + return action.topology + case ADD_ROOM: + return produce(state, (draft) => { + const { room } = action + draft.rooms.push(room._id) + }) + case DELETE_ROOM: + return produce(state, (draft) => { + const { roomId } = action + const index = draft.rooms.indexOf(roomId) + draft.rooms.splice(index, 1) + }) + default: + return state + } +} + +export default topology 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 9ddc564d..0fabdb6d 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/index.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/index.js @@ -1,51 +1,7 @@ -import { takeEvery } from 'redux-saga/effects' -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 { - onAddMachine, - onAddRackToTile, - onAddTile, - onAddTopology, - onAddUnit, - onCancelNewRoomConstruction, - onDeleteMachine, - onDeleteRack, - onDeleteRoom, - onDeleteTile, - onDeleteUnit, - onEditRackName, - onEditRoomName, - onStartNewRoomConstruction, - onOpenTopology, -} from './topology' -import { ADD_TOPOLOGY, OPEN_TOPOLOGY } from '../actions/topologies' -import { onAddPrefab } from './prefabs' -import { ADD_PREFAB } from '../actions/prefabs' +import { fork } from 'redux-saga/effects' +import { watchServer, updateServer } from './topology' export default function* rootSaga() { - yield takeEvery(OPEN_TOPOLOGY, onOpenTopology) - - yield takeEvery(ADD_TOPOLOGY, onAddTopology) - 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_PREFAB, onAddPrefab) + yield fork(watchServer) + yield fork(updateServer) } diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js b/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js deleted file mode 100644 index f717d878..00000000 --- a/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js +++ /dev/null @@ -1,18 +0,0 @@ -import { call, put, select, getContext } from 'redux-saga/effects' -import { addToStore } from '../actions/objects' -import { addPrefab } from '../../api/prefabs' -import { Rack } from '../../util/topology-schema' -import { denormalize } from 'normalizr' - -export function* onAddPrefab({ name, tileId }) { - try { - const objects = yield select((state) => state.objects) - const rack = objects.rack[objects.tile[tileId].rack] - const prefabRack = denormalize(rack, Rack, objects) - const auth = yield getContext('auth') - const prefab = yield call(() => addPrefab(auth, { name, rack: prefabRack })) - yield put(addToStore('prefab', prefab)) - } catch (error) { - console.error(error) - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/query.js b/opendc-web/opendc-web-ui/src/redux/sagas/query.js deleted file mode 100644 index 787006c7..00000000 --- a/opendc-web/opendc-web-ui/src/redux/sagas/query.js +++ /dev/null @@ -1,41 +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 { MutationObserver } from 'react-query' -import { getContext, call } from 'redux-saga/effects' - -/** - * Fetch the query with the specified key. - */ -export function* fetchQuery(key, options) { - const queryClient = yield getContext('queryClient') - return yield call([queryClient, queryClient.fetchQuery], key, options) -} - -/** - * Perform a mutation with the specified key. - */ -export function* mutate(key, object, options) { - const queryClient = yield getContext('queryClient') - const mutationObserver = new MutationObserver(queryClient, { mutationKey: key }) - return yield call([mutationObserver, mutationObserver.mutate], object, options) -} 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 fb6f7f0d..f40cff28 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -1,287 +1,75 @@ +import { QueryObserver, MutationObserver } from 'react-query' import { normalize, denormalize } from 'normalizr' -import { put, select } from 'redux-saga/effects' +import { select, put, take, race, getContext, call } from 'redux-saga/effects' +import { eventChannel } from 'redux-saga' import { Topology } from '../../util/topology-schema' -import { goDownOneInteractionLevel } from '../actions/interaction-level' -import { - addIdToStoreObjectListProp, - addPropToStoreObject, - addToStore, - removeIdFromStoreObjectListProp, -} from '../actions/objects' -import { storeTopology } from '../actions/topologies' -import { - cancelNewRoomConstructionSucceeded, - setCurrentTopology, - startNewRoomConstructionSucceeded, -} from '../actions/topology/building' -import { - DEFAULT_RACK_POWER_CAPACITY, - DEFAULT_RACK_SLOT_CAPACITY, - MAX_NUM_UNITS_PER_MACHINE, -} from '../../components/topologies/map/MapConstants' -import { uuid } from 'uuidv4' -import { fetchQuery, mutate } from './query' +import { storeTopology, OPEN_TOPOLOGY } from '../actions/topologies' /** - * Fetches and normalizes the topology with the specified identifier. + * Update the topology on the server. */ -function* fetchAndStoreTopology(id) { - let topology = yield select((state) => state.objects.topology[id]) - if (!topology) { - const newTopology = yield fetchQuery(['topologies', id]) - const { entities } = normalize(newTopology, Topology) - yield put(storeTopology(entities)) - } - - return topology -} - -/** - * Synchronize the topology with the specified identifier with the server. - */ -function* updateTopologyOnServer(id) { - const topology = yield denormalizeTopology(id) - yield mutate('updateTopology', topology) -} - -/** - * Denormalizes the topology representation in order to be stored on the server. - */ -function* denormalizeTopology(id) { - const objects = yield select((state) => state.objects) - const topology = objects.topology[id] - return denormalize(topology, Topology, objects) -} - -export function* onOpenTopology({ id }) { - try { - yield fetchAndStoreTopology(id) - yield put(setCurrentTopology(id)) - } catch (error) { - console.error(error) - } -} - -export function* onAddTopology({ projectId, duplicateId, name }) { - try { - let topologyToBeCreated - if (duplicateId) { - topologyToBeCreated = yield denormalizeTopology(duplicateId) - topologyToBeCreated = { ...topologyToBeCreated, name } - delete topologyToBeCreated._id - } else { - topologyToBeCreated = { name, rooms: [] } - } - - const topology = yield mutate('addTopology', { ...topologyToBeCreated, projectId }) - yield fetchAndStoreTopology(topology._id) - yield put(setCurrentTopology(topology._id)) - } catch (error) { - console.error(error) - } -} - -export function* onStartNewRoomConstruction() { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const room = { - _id: uuid(), - name: 'Room', - topologyId, - tiles: [], - } - yield put(addToStore('room', room)) - yield put(addIdToStoreObjectListProp('topology', topologyId, 'rooms', 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, 'rooms', roomId)) - // TODO remove room from store, too - yield updateTopologyOnServer(topologyId) - yield put(cancelNewRoomConstructionSucceeded()) - } catch (error) { - console.error(error) - } -} +export function* updateServer() { + const queryClient = yield getContext('queryClient') + const mutationObserver = new MutationObserver(queryClient, { mutationKey: 'updateTopology' }) + + while (true) { + yield take( + (action) => + action.type.startsWith('EDIT') || action.type.startsWith('ADD') || action.type.startsWith('DELETE') + ) + const topology = yield select((state) => state.topology) -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, + if (!topology.root) { + continue } - yield put(addToStore('tile', tile)) - yield put(addIdToStoreObjectListProp('room', roomId, 'tiles', 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, 'tiles', action.tileId)) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} -export function* onEditRoomName({ roomId, name }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - yield put(addPropToStoreObject('room', roomId, { name })) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) + const denormalizedTopology = denormalize(topology.root, Topology, topology) + yield call([mutationObserver, mutationObserver.mutate], denormalizedTopology) } } -export function* onDeleteRoom({ roomId }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - yield put(goDownOneInteractionLevel()) - yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'rooms', roomId)) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onEditRackName({ rackId, name }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - yield put(addPropToStoreObject('rack', rackId, { name })) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onDeleteRack({ tileId }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - yield put(goDownOneInteractionLevel()) - yield put(addPropToStoreObject('tile', tileId, { rack: undefined })) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onAddRackToTile({ tileId }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const rack = { - _id: uuid(), - name: 'Rack', - tileId, - capacity: DEFAULT_RACK_SLOT_CAPACITY, - powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, - machines: [], - } - yield put(addToStore('rack', rack)) - yield put(addPropToStoreObject('tile', tileId, { rack: rack._id })) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onAddMachine({ rackId, position }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const rack = yield select((state) => state.objects.rack[rackId]) - - const machine = { - _id: uuid(), - rackId, - position, - cpus: [], - gpus: [], - memories: [], - storages: [], +/** + * Watch the topology on the server for changes. + */ +export function* watchServer() { + let { id } = yield take(OPEN_TOPOLOGY) + while (true) { + const channel = yield queryObserver(id) + + while (true) { + const [action, response] = yield race([take(OPEN_TOPOLOGY), take(channel)]) + + if (action) { + id = action.id + break + } + + const { isFetched, data } = response + // Only update the topology on the client-side when a new topology was fetched + if (isFetched) { + const { result: topologyId, entities } = normalize(data, Topology) + yield put(storeTopology(entities.topologies[topologyId], entities)) + } } - yield put(addToStore('machine', machine)) - - const machineIds = [...rack.machines, machine._id] - yield put(addPropToStoreObject('rack', rackId, { machines: machineIds })) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onDeleteMachine({ rackId, position }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const rack = yield select((state) => state.objects.rack[rackId]) - yield put(goDownOneInteractionLevel()) - yield put( - addPropToStoreObject('rack', rackId, { 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({ machineId, unitType, id }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const machine = yield select((state) => state.objects.machine[machineId]) - - if (machine[unitMapping[unitType]].length >= MAX_NUM_UNITS_PER_MACHINE) { - return - } +/** + * Observe changes for the topology with the specified identifier. + */ +function* queryObserver(id) { + const queryClient = yield getContext('queryClient') + const observer = new QueryObserver(queryClient, { queryKey: ['topologies', id] }) - const units = [...machine[unitMapping[unitType]], id] - yield put( - addPropToStoreObject('machine', machine._id, { - [unitMapping[unitType]]: units, - }) - ) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} + return eventChannel((emitter) => { + const unsubscribe = observer.subscribe((result) => { + emitter(result) + }) -export function* onDeleteUnit({ machineId, unitType, index }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const machine = yield select((state) => state.objects.machine[machineId]) - const unitIds = machine[unitMapping[unitType]].slice() - unitIds.splice(index, 1) + // Update result to make sure we did not miss any query updates + // between creating the observer and subscribing to it. + observer.updateResult() - yield put( - addPropToStoreObject('machine', machine._id, { - [unitMapping[unitType]]: unitIds, - }) - ) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } + return unsubscribe + }) } -- cgit v1.2.3