diff options
Diffstat (limited to 'opendc-web/opendc-web-ui/src')
241 files changed, 8335 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-ui/src/actions/auth.js b/opendc-web/opendc-web-ui/src/actions/auth.js new file mode 100644 index 00000000..38c1a782 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/interaction-level.js b/opendc-web/opendc-web-ui/src/actions/interaction-level.js new file mode 100644 index 00000000..ff6b1fa3 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/map.js b/opendc-web/opendc-web-ui/src/actions/map.js new file mode 100644 index 00000000..0d49d849 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/actions/map.js @@ -0,0 +1,82 @@ +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/actions/modals/portfolios.js b/opendc-web/opendc-web-ui/src/actions/modals/portfolios.js new file mode 100644 index 00000000..f6dce2e3 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/actions/modals/portfolios.js @@ -0,0 +1,14 @@ +export const OPEN_NEW_PORTFOLIO_MODAL = 'OPEN_NEW_PORTFOLIO_MODAL' +export const CLOSE_NEW_PORTFOLIO_MODAL = 'CLOSE_PORTFOLIO_MODAL' + +export function openNewPortfolioModal() { + return { + type: OPEN_NEW_PORTFOLIO_MODAL, + } +} + +export function closeNewPortfolioModal() { + return { + type: CLOSE_NEW_PORTFOLIO_MODAL, + } +} diff --git a/opendc-web/opendc-web-ui/src/actions/modals/prefabs.js b/opendc-web/opendc-web-ui/src/actions/modals/prefabs.js new file mode 100644 index 00000000..826565d2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/actions/modals/prefabs.js @@ -0,0 +1,14 @@ +export const OPEN_NEW_PREFAB_MODAL = 'OPEN_NEW_PREFAB_MODAL' +export const CLOSE_NEW_PREFAB_MODAL = 'CLOSE_PREFAB_MODAL' + +export function openNewPrefabModal() { + return { + type: OPEN_NEW_PREFAB_MODAL, + } +} + +export function closeNewPrefabModal() { + return { + type: CLOSE_NEW_PREFAB_MODAL, + } +} diff --git a/opendc-web/opendc-web-ui/src/actions/modals/profile.js b/opendc-web/opendc-web-ui/src/actions/modals/profile.js new file mode 100644 index 00000000..39c72c03 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/actions/modals/profile.js @@ -0,0 +1,14 @@ +export const OPEN_DELETE_PROFILE_MODAL = 'OPEN_DELETE_PROFILE_MODAL' +export const CLOSE_DELETE_PROFILE_MODAL = 'CLOSE_DELETE_PROFILE_MODAL' + +export function openDeleteProfileModal() { + return { + type: OPEN_DELETE_PROFILE_MODAL, + } +} + +export function closeDeleteProfileModal() { + return { + type: CLOSE_DELETE_PROFILE_MODAL, + } +} diff --git a/opendc-web/opendc-web-ui/src/actions/modals/projects.js b/opendc-web/opendc-web-ui/src/actions/modals/projects.js new file mode 100644 index 00000000..d1043cbb --- /dev/null +++ b/opendc-web/opendc-web-ui/src/actions/modals/projects.js @@ -0,0 +1,14 @@ +export const OPEN_NEW_PROJECT_MODAL = 'OPEN_NEW_PROJECT_MODAL' +export const CLOSE_NEW_PROJECT_MODAL = 'CLOSE_PROJECT_MODAL' + +export function openNewProjectModal() { + return { + type: OPEN_NEW_PROJECT_MODAL, + } +} + +export function closeNewProjectModal() { + return { + type: CLOSE_NEW_PROJECT_MODAL, + } +} diff --git a/opendc-web/opendc-web-ui/src/actions/modals/scenarios.js b/opendc-web/opendc-web-ui/src/actions/modals/scenarios.js new file mode 100644 index 00000000..b71cb27b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/actions/modals/scenarios.js @@ -0,0 +1,14 @@ +export const OPEN_NEW_SCENARIO_MODAL = 'OPEN_NEW_SCENARIO_MODAL' +export const CLOSE_NEW_SCENARIO_MODAL = 'CLOSE_SCENARIO_MODAL' + +export function openNewScenarioModal() { + return { + type: OPEN_NEW_SCENARIO_MODAL, + } +} + +export function closeNewScenarioModal() { + return { + type: CLOSE_NEW_SCENARIO_MODAL, + } +} diff --git a/opendc-web/opendc-web-ui/src/actions/modals/topology.js b/opendc-web/opendc-web-ui/src/actions/modals/topology.js new file mode 100644 index 00000000..b5fecac1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/actions/modals/topology.js @@ -0,0 +1,84 @@ +export const OPEN_NEW_TOPOLOGY_MODAL = 'OPEN_NEW_TOPOLOGY_MODAL' +export const CLOSE_NEW_TOPOLOGY_MODAL = 'CLOSE_NEW_TOPOLOGY_MODAL' +export const OPEN_EDIT_ROOM_NAME_MODAL = 'OPEN_EDIT_ROOM_NAME_MODAL' +export const CLOSE_EDIT_ROOM_NAME_MODAL = 'CLOSE_EDIT_ROOM_NAME_MODAL' +export const OPEN_DELETE_ROOM_MODAL = 'OPEN_DELETE_ROOM_MODAL' +export const CLOSE_DELETE_ROOM_MODAL = 'CLOSE_DELETE_ROOM_MODAL' +export const OPEN_EDIT_RACK_NAME_MODAL = 'OPEN_EDIT_RACK_NAME_MODAL' +export const CLOSE_EDIT_RACK_NAME_MODAL = 'CLOSE_EDIT_RACK_NAME_MODAL' +export const OPEN_DELETE_RACK_MODAL = 'OPEN_DELETE_RACK_MODAL' +export const CLOSE_DELETE_RACK_MODAL = 'CLOSE_DELETE_RACK_MODAL' +export const OPEN_DELETE_MACHINE_MODAL = 'OPEN_DELETE_MACHINE_MODAL' +export const CLOSE_DELETE_MACHINE_MODAL = 'CLOSE_DELETE_MACHINE_MODAL' + +export function openNewTopologyModal() { + return { + type: OPEN_NEW_TOPOLOGY_MODAL, + } +} + +export function closeNewTopologyModal() { + return { + type: CLOSE_NEW_TOPOLOGY_MODAL, + } +} + +export function openEditRoomNameModal() { + return { + type: OPEN_EDIT_ROOM_NAME_MODAL, + } +} + +export function closeEditRoomNameModal() { + return { + type: CLOSE_EDIT_ROOM_NAME_MODAL, + } +} + +export function openDeleteRoomModal() { + return { + type: OPEN_DELETE_ROOM_MODAL, + } +} + +export function closeDeleteRoomModal() { + return { + type: CLOSE_DELETE_ROOM_MODAL, + } +} + +export function openEditRackNameModal() { + return { + type: OPEN_EDIT_RACK_NAME_MODAL, + } +} + +export function closeEditRackNameModal() { + return { + type: CLOSE_EDIT_RACK_NAME_MODAL, + } +} + +export function openDeleteRackModal() { + return { + type: OPEN_DELETE_RACK_MODAL, + } +} + +export function closeDeleteRackModal() { + return { + type: CLOSE_DELETE_RACK_MODAL, + } +} + +export function openDeleteMachineModal() { + return { + type: OPEN_DELETE_MACHINE_MODAL, + } +} + +export function closeDeleteMachineModal() { + return { + type: CLOSE_DELETE_MACHINE_MODAL, + } +} diff --git a/opendc-web/opendc-web-ui/src/actions/objects.js b/opendc-web/opendc-web-ui/src/actions/objects.js new file mode 100644 index 00000000..7b648b18 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/portfolios.js b/opendc-web/opendc-web-ui/src/actions/portfolios.js new file mode 100644 index 00000000..d37886d8 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/prefabs.js b/opendc-web/opendc-web-ui/src/actions/prefabs.js new file mode 100644 index 00000000..c112feed --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/projects.js b/opendc-web/opendc-web-ui/src/actions/projects.js new file mode 100644 index 00000000..add0f242 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/actions/projects.js @@ -0,0 +1,52 @@ +export const SET_AUTH_VISIBILITY_FILTER = 'SET_AUTH_VISIBILITY_FILTER' +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 setAuthVisibilityFilter(filter) { + return { + type: SET_AUTH_VISIBILITY_FILTER, + filter, + } +} + +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/actions/scenarios.js b/opendc-web/opendc-web-ui/src/actions/scenarios.js new file mode 100644 index 00000000..c8a90762 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/topologies.js b/opendc-web/opendc-web-ui/src/actions/topologies.js new file mode 100644 index 00000000..dcce3b7d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/topology/building.js b/opendc-web/opendc-web-ui/src/actions/topology/building.js new file mode 100644 index 00000000..72deda6f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/topology/machine.js b/opendc-web/opendc-web-ui/src/actions/topology/machine.js new file mode 100644 index 00000000..17ccce5d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/topology/rack.js b/opendc-web/opendc-web-ui/src/actions/topology/rack.js new file mode 100644 index 00000000..b117402e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/topology/room.js b/opendc-web/opendc-web-ui/src/actions/topology/room.js new file mode 100644 index 00000000..52cba680 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/actions/users.js b/opendc-web/opendc-web-ui/src/actions/users.js new file mode 100644 index 00000000..4868ac34 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/api/index.js b/opendc-web/opendc-web-ui/src/api/index.js new file mode 100644 index 00000000..cefcb2c5 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/index.js @@ -0,0 +1,13 @@ +import { sendSocketRequest } from './socket' + +export function sendRequest(request) { + return new Promise((resolve, reject) => { + sendSocketRequest(request, (response) => { + if (response.status.code === 200) { + resolve(response.content) + } else { + reject(response) + } + }) + }) +} diff --git a/opendc-web/opendc-web-ui/src/api/routes/portfolios.js b/opendc-web/opendc-web-ui/src/api/routes/portfolios.js new file mode 100644 index 00000000..7c9ea02a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/routes/portfolios.js @@ -0,0 +1,42 @@ +import { deleteById, getById } from './util' +import { sendRequest } from '../index' + +export function addPortfolio(projectId, portfolio) { + return sendRequest({ + path: '/projects/{projectId}/portfolios', + method: 'POST', + parameters: { + body: { + portfolio, + }, + path: { + projectId, + }, + query: {}, + }, + }) +} + +export function getPortfolio(portfolioId) { + return getById('/portfolios/{portfolioId}', { portfolioId }) +} + +export function updatePortfolio(portfolioId, portfolio) { + return sendRequest({ + path: '/portfolios/{projectId}', + method: 'POST', + parameters: { + body: { + portfolio, + }, + path: { + portfolioId, + }, + query: {}, + }, + }) +} + +export function deletePortfolio(portfolioId) { + return deleteById('/portfolios/{portfolioId}', { portfolioId }) +} diff --git a/opendc-web/opendc-web-ui/src/api/routes/prefabs.js b/opendc-web/opendc-web-ui/src/api/routes/prefabs.js new file mode 100644 index 00000000..8a1debfa --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/routes/prefabs.js @@ -0,0 +1,40 @@ +import { sendRequest } from '../index' +import { deleteById, getById } from './util' + +export function getPrefab(prefabId) { + return getById('/prefabs/{prefabId}', { prefabId }) +} + +export function addPrefab(prefab) { + return sendRequest({ + path: '/prefabs', + method: 'POST', + parameters: { + body: { + prefab, + }, + path: {}, + query: {}, + }, + }) +} + +export function updatePrefab(prefab) { + return sendRequest({ + path: '/prefabs/{prefabId}', + method: 'PUT', + parameters: { + body: { + prefab, + }, + path: { + prefabId: prefab._id, + }, + query: {}, + }, + }) +} + +export function deletePrefab(prefabId) { + return deleteById('/prefabs/{prefabId}', { prefabId }) +} diff --git a/opendc-web/opendc-web-ui/src/api/routes/projects.js b/opendc-web/opendc-web-ui/src/api/routes/projects.js new file mode 100644 index 00000000..4109079c --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/routes/projects.js @@ -0,0 +1,40 @@ +import { sendRequest } from '../index' +import { deleteById, getById } from './util' + +export function getProject(projectId) { + return getById('/projects/{projectId}', { projectId }) +} + +export function addProject(project) { + return sendRequest({ + path: '/projects', + method: 'POST', + parameters: { + body: { + project, + }, + path: {}, + query: {}, + }, + }) +} + +export function updateProject(project) { + return sendRequest({ + path: '/projects/{projectId}', + method: 'PUT', + parameters: { + body: { + project, + }, + path: { + projectId: project._id, + }, + query: {}, + }, + }) +} + +export function deleteProject(projectId) { + return deleteById('/projects/{projectId}', { projectId }) +} diff --git a/opendc-web/opendc-web-ui/src/api/routes/scenarios.js b/opendc-web/opendc-web-ui/src/api/routes/scenarios.js new file mode 100644 index 00000000..ab2e8b86 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/routes/scenarios.js @@ -0,0 +1,42 @@ +import { deleteById, getById } from './util' +import { sendRequest } from '../index' + +export function addScenario(portfolioId, scenario) { + return sendRequest({ + path: '/portfolios/{portfolioId}/scenarios', + method: 'POST', + parameters: { + body: { + scenario, + }, + path: { + portfolioId, + }, + query: {}, + }, + }) +} + +export function getScenario(scenarioId) { + return getById('/scenarios/{scenarioId}', { scenarioId }) +} + +export function updateScenario(scenarioId, scenario) { + return sendRequest({ + path: '/scenarios/{projectId}', + method: 'POST', + parameters: { + body: { + scenario, + }, + path: { + scenarioId, + }, + query: {}, + }, + }) +} + +export function deleteScenario(scenarioId) { + return deleteById('/scenarios/{scenarioId}', { scenarioId }) +} diff --git a/opendc-web/opendc-web-ui/src/api/routes/schedulers.js b/opendc-web/opendc-web-ui/src/api/routes/schedulers.js new file mode 100644 index 00000000..4481fb2a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/routes/schedulers.js @@ -0,0 +1,5 @@ +import { getAll } from './util' + +export function getAllSchedulers() { + return getAll('/schedulers') +} diff --git a/opendc-web/opendc-web-ui/src/api/routes/token-signin.js b/opendc-web/opendc-web-ui/src/api/routes/token-signin.js new file mode 100644 index 00000000..d6cff570 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/routes/token-signin.js @@ -0,0 +1,10 @@ +export function performTokenSignIn(token) { + const apiUrl = process.env.REACT_APP_API_BASE_URL || '' + + return fetch(`${apiUrl}/tokensignin`, { + method: 'POST', + body: new URLSearchParams({ + idtoken: token, + }), + }).then((res) => res.json()) +} diff --git a/opendc-web/opendc-web-ui/src/api/routes/topologies.js b/opendc-web/opendc-web-ui/src/api/routes/topologies.js new file mode 100644 index 00000000..a8f0d6b1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/routes/topologies.js @@ -0,0 +1,42 @@ +import { deleteById, getById } from './util' +import { sendRequest } from '../index' + +export function addTopology(topology) { + return sendRequest({ + path: '/projects/{projectId}/topologies', + method: 'POST', + parameters: { + body: { + topology, + }, + path: { + projectId: topology.projectId, + }, + query: {}, + }, + }) +} + +export function getTopology(topologyId) { + return getById('/topologies/{topologyId}', { topologyId }) +} + +export function updateTopology(topology) { + return sendRequest({ + path: '/topologies/{topologyId}', + method: 'PUT', + parameters: { + body: { + topology, + }, + path: { + topologyId: topology._id, + }, + query: {}, + }, + }) +} + +export function deleteTopology(topologyId) { + return deleteById('/topologies/{topologyId}', { topologyId }) +} diff --git a/opendc-web/opendc-web-ui/src/api/routes/traces.js b/opendc-web/opendc-web-ui/src/api/routes/traces.js new file mode 100644 index 00000000..67895a87 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/routes/traces.js @@ -0,0 +1,5 @@ +import { getAll } from './util' + +export function getAllTraces() { + return getAll('/traces') +} diff --git a/opendc-web/opendc-web-ui/src/api/routes/users.js b/opendc-web/opendc-web-ui/src/api/routes/users.js new file mode 100644 index 00000000..3028f3f7 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/routes/users.js @@ -0,0 +1,48 @@ +import { sendRequest } from '../index' +import { deleteById } from './util' + +export function getUserByEmail(email) { + return sendRequest({ + path: '/users', + method: 'GET', + parameters: { + body: {}, + path: {}, + query: { + email, + }, + }, + }) +} + +export function addUser(user) { + return sendRequest({ + path: '/users', + method: 'POST', + parameters: { + body: { + user, + }, + path: {}, + query: {}, + }, + }) +} + +export function getUser(userId) { + return sendRequest({ + path: '/users/{userId}', + method: 'GET', + parameters: { + body: {}, + path: { + userId, + }, + query: {}, + }, + }) +} + +export function deleteUser(userId) { + return deleteById('/users/{userId}', { userId }) +} diff --git a/opendc-web/opendc-web-ui/src/api/routes/util.js b/opendc-web/opendc-web-ui/src/api/routes/util.js new file mode 100644 index 00000000..67e7173b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/routes/util.js @@ -0,0 +1,37 @@ +import { sendRequest } from '../index' + +export function getAll(path) { + return sendRequest({ + path, + method: 'GET', + parameters: { + body: {}, + path: {}, + query: {}, + }, + }) +} + +export function getById(path, pathObject) { + return sendRequest({ + path, + method: 'GET', + parameters: { + body: {}, + path: pathObject, + query: {}, + }, + }) +} + +export function deleteById(path, pathObject) { + return sendRequest({ + path, + method: 'DELETE', + parameters: { + body: {}, + path: pathObject, + query: {}, + }, + }) +} diff --git a/opendc-web/opendc-web-ui/src/api/socket.js b/opendc-web/opendc-web-ui/src/api/socket.js new file mode 100644 index 00000000..1c432167 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/api/socket.js @@ -0,0 +1,50 @@ +import io from 'socket.io-client' +import { getAuthToken } from '../auth/index' + +let socket +let requestIdCounter = 0 +const callbacks = {} + +export function setupSocketConnection(onConnect) { + const apiUrl = + process.env.REACT_APP_API_BASE_URL || + `${window.location.protocol}//${window.location.hostname}:${window.location.port}` + + socket = io.connect(apiUrl) + socket.on('connect', onConnect) + socket.on('response', onSocketResponse) +} + +export function sendSocketRequest(request, callback) { + if (!socket.connected) { + console.error('Attempted to send request over unconnected socket') + return + } + + const newId = requestIdCounter++ + callbacks[newId] = callback + + request.id = newId + request.token = getAuthToken() + + if (!request.isRootRoute) { + request.path = '/v2' + request.path + } + + socket.emit('request', request) + + if (process.env.NODE_ENV !== 'production') { + console.log('Sent socket request:', request) + } +} + +function onSocketResponse(json) { + const response = JSON.parse(json) + + if (process.env.NODE_ENV !== 'production') { + console.log('Received socket response:', response) + } + + callbacks[response.id](response) + delete callbacks[response.id] +} diff --git a/opendc-web/opendc-web-ui/src/auth/index.js b/opendc-web/opendc-web-ui/src/auth/index.js new file mode 100644 index 00000000..b5953990 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/auth/index.js @@ -0,0 +1,57 @@ +import { LOG_IN_SUCCEEDED, LOG_OUT } from '../actions/auth' +import { DELETE_CURRENT_USER_SUCCEEDED } from '../actions/users' + +const getAuthObject = () => { + const authItem = localStorage.getItem('auth') + if (!authItem || authItem === '{}') { + return undefined + } + return JSON.parse(authItem) +} + +export const userIsLoggedIn = () => { + const authObj = getAuthObject() + + if (!authObj || !authObj.googleId) { + return false + } + + const currentTime = new Date().getTime() + return parseInt(authObj.expiresAt, 10) - currentTime > 0 +} + +export const getAuthToken = () => { + const authObj = getAuthObject() + if (!authObj) { + return undefined + } + + return authObj.authToken +} + +export const saveAuthLocalStorage = (payload) => { + localStorage.setItem('auth', JSON.stringify(payload)) +} + +export const clearAuthLocalStorage = () => { + localStorage.setItem('auth', '') +} + +export const authRedirectMiddleware = (store) => (next) => (action) => { + switch (action.type) { + case LOG_IN_SUCCEEDED: + saveAuthLocalStorage(action.payload) + window.location.href = '/projects' + break + case LOG_OUT: + case DELETE_CURRENT_USER_SUCCEEDED: + clearAuthLocalStorage() + window.location.href = '/' + break + default: + next(action) + return + } + + next(action) +} diff --git a/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js b/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js new file mode 100644 index 00000000..7efea9b0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/LoadingScreen.js @@ -0,0 +1,11 @@ +import React from 'react' +import FontAwesome from 'react-fontawesome' + +const LoadingScreen = () => ( + <div className="display-4"> + <FontAwesome name="refresh" className="mr-4" spin /> + Loading your project... + </div> +) + +export default LoadingScreen diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js new file mode 100644 index 00000000..d6ea1f84 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/MapConstants.js @@ -0,0 +1,28 @@ +export const MAP_SIZE = 50 +export const TILE_SIZE_IN_PIXELS = 100 +export const TILE_SIZE_IN_METERS = 0.5 +export const MAP_SIZE_IN_PIXELS = MAP_SIZE * TILE_SIZE_IN_PIXELS + +export const OBJECT_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 5 +export const TILE_PLUS_MARGIN_IN_PIXELS = TILE_SIZE_IN_PIXELS / 3 +export const OBJECT_SIZE_IN_PIXELS = TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2 + +export const GRID_LINE_WIDTH_IN_PIXELS = 2 +export const WALL_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 8 +export const OBJECT_BORDER_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 12 +export const TILE_PLUS_WIDTH_IN_PIXELS = TILE_SIZE_IN_PIXELS / 10 + +export const SIDEBAR_WIDTH = 350 +export const VIEWPORT_PADDING = 50 + +export const RACK_FILL_ICON_WIDTH = OBJECT_SIZE_IN_PIXELS / 3 +export const RACK_FILL_ICON_OPACITY = 0.8 + +export const MAP_MOVE_PIXELS_PER_EVENT = 20 +export const MAP_SCALE_PER_EVENT = 1.1 +export const MAP_MIN_SCALE = 0.5 +export const MAP_MAX_SCALE = 1.5 + +export const MAX_NUM_UNITS_PER_MACHINE = 6 +export const DEFAULT_RACK_SLOT_CAPACITY = 42 +export const DEFAULT_RACK_POWER_CAPACITY = 10000 diff --git a/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js new file mode 100644 index 00000000..2cd0ed6e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/MapStageComponent.js @@ -0,0 +1,103 @@ +import React from 'react' +import { Stage } from 'react-konva' +import { Shortcuts } from 'react-shortcuts' +import MapLayer from '../../../containers/app/map/layers/MapLayer' +import ObjectHoverLayer from '../../../containers/app/map/layers/ObjectHoverLayer' +import RoomHoverLayer from '../../../containers/app/map/layers/RoomHoverLayer' +import { NAVBAR_HEIGHT } from '../../navigation/Navbar' +import { MAP_MOVE_PIXELS_PER_EVENT } from './MapConstants' +import { Provider } from 'react-redux' +import { store } from '../../../store/configure-store' + +class MapStageComponent extends React.Component { + state = { + mouseX: 0, + mouseY: 0, + } + + constructor(props) { + super(props) + + this.updateDimensions = this.updateDimensions.bind(this) + this.updateScale = this.updateScale.bind(this) + } + + componentDidMount() { + this.updateDimensions() + + window.addEventListener('resize', this.updateDimensions) + window.addEventListener('wheel', this.updateScale) + + window['exportCanvasToImage'] = () => { + const download = document.createElement('a') + download.href = this.stage.getStage().toDataURL() + download.download = 'opendc-canvas-export-' + Date.now() + '.png' + download.click() + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.updateDimensions) + window.removeEventListener('wheel', this.updateScale) + } + + updateDimensions() { + this.props.setMapDimensions(window.innerWidth, window.innerHeight - NAVBAR_HEIGHT) + } + + updateScale(e) { + e.preventDefault() + this.props.zoomInOnPosition(e.deltaY < 0, this.state.mouseX, this.state.mouseY) + } + + updateMousePosition() { + const mousePos = this.stage.getStage().getPointerPosition() + this.setState({ mouseX: mousePos.x, mouseY: mousePos.y }) + } + + handleShortcuts(action) { + switch (action) { + case 'MOVE_LEFT': + this.moveWithDelta(MAP_MOVE_PIXELS_PER_EVENT, 0) + break + case 'MOVE_RIGHT': + this.moveWithDelta(-MAP_MOVE_PIXELS_PER_EVENT, 0) + break + case 'MOVE_UP': + this.moveWithDelta(0, MAP_MOVE_PIXELS_PER_EVENT) + break + case 'MOVE_DOWN': + this.moveWithDelta(0, -MAP_MOVE_PIXELS_PER_EVENT) + break + default: + break + } + } + + moveWithDelta(deltaX, deltaY) { + this.props.setMapPositionWithBoundsCheck(this.props.mapPosition.x + deltaX, this.props.mapPosition.y + deltaY) + } + + render() { + return ( + <Shortcuts name="MAP" handler={this.handleShortcuts.bind(this)} targetNodeSelector="body"> + <Stage + ref={(stage) => { + this.stage = stage + }} + width={this.props.mapDimensions.width} + height={this.props.mapDimensions.height} + onMouseMove={this.updateMousePosition.bind(this)} + > + <Provider store={store}> + <MapLayer /> + <RoomHoverLayer mouseX={this.state.mouseX} mouseY={this.state.mouseY} /> + <ObjectHoverLayer mouseX={this.state.mouseX} mouseY={this.state.mouseY} /> + </Provider> + </Stage> + </Shortcuts> + ) + } +} + +export default MapStageComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js new file mode 100644 index 00000000..8487f47b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ExportCanvasComponent.js @@ -0,0 +1,13 @@ +import React from 'react' + +const ExportCanvasComponent = () => ( + <button + className="btn btn-success btn-circle btn-sm" + title="Export Canvas to PNG Image" + onClick={() => window['exportCanvasToImage']()} + > + <span className="fa fa-camera" /> + </button> +) + +export default ExportCanvasComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js new file mode 100644 index 00000000..7cbb45c0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.js @@ -0,0 +1,11 @@ +import React from 'react' +import { TILE_SIZE_IN_METERS, TILE_SIZE_IN_PIXELS } from '../MapConstants' +import './ScaleIndicatorComponent.sass' + +const ScaleIndicatorComponent = ({ scale }) => ( + <div className="scale-indicator" style={{ width: TILE_SIZE_IN_PIXELS * scale }}> + {TILE_SIZE_IN_METERS}m + </div> +) + +export default ScaleIndicatorComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.sass b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.sass new file mode 100644 index 00000000..03a72c99 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ScaleIndicatorComponent.sass @@ -0,0 +1,9 @@ +.scale-indicator + position: absolute + right: 10px + bottom: 10px + z-index: 50 + + border: solid 2px #212529 + border-top: none + border-left: none diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js new file mode 100644 index 00000000..f372734d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.js @@ -0,0 +1,13 @@ +import React from 'react' +import ZoomControlContainer from '../../../../containers/app/map/controls/ZoomControlContainer' +import ExportCanvasComponent from './ExportCanvasComponent' +import './ToolPanelComponent.sass' + +const ToolPanelComponent = () => ( + <div className="tool-panel"> + <ZoomControlContainer /> + <ExportCanvasComponent /> + </div> +) + +export default ToolPanelComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.sass b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.sass new file mode 100644 index 00000000..8b27d24a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ToolPanelComponent.sass @@ -0,0 +1,5 @@ +.tool-panel + position: absolute + left: 10px + bottom: 10px + z-index: 50 diff --git a/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js new file mode 100644 index 00000000..65944bea --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/controls/ZoomControlComponent.js @@ -0,0 +1,24 @@ +import React from 'react' + +const ZoomControlComponent = ({ zoomInOnCenter }) => { + return ( + <span> + <button + className="btn btn-default btn-circle btn-sm mr-1" + title="Zoom in" + onClick={() => zoomInOnCenter(true)} + > + <span className="fa fa-plus" /> + </button> + <button + className="btn btn-default btn-circle btn-sm mr-1" + title="Zoom out" + onClick={() => zoomInOnCenter(false)} + > + <span className="fa fa-minus" /> + </button> + </span> + ) +} + +export default ZoomControlComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/Backdrop.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/Backdrop.js new file mode 100644 index 00000000..8ccfe584 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/Backdrop.js @@ -0,0 +1,8 @@ +import React from 'react' +import { Rect } from 'react-konva' +import { BACKDROP_COLOR } from '../../../../util/colors' +import { MAP_SIZE_IN_PIXELS } from '../MapConstants' + +const Backdrop = () => <Rect x={0} y={0} width={MAP_SIZE_IN_PIXELS} height={MAP_SIZE_IN_PIXELS} fill={BACKDROP_COLOR} /> + +export default Backdrop diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/GrayLayer.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/GrayLayer.js new file mode 100644 index 00000000..c54a34ad --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/GrayLayer.js @@ -0,0 +1,17 @@ +import React from 'react' +import { Rect } from 'react-konva' +import { GRAYED_OUT_AREA_COLOR } from '../../../../util/colors' +import { MAP_SIZE_IN_PIXELS } from '../MapConstants' + +const GrayLayer = ({ onClick }) => ( + <Rect + x={0} + y={0} + width={MAP_SIZE_IN_PIXELS} + height={MAP_SIZE_IN_PIXELS} + fill={GRAYED_OUT_AREA_COLOR} + onClick={onClick} + /> +) + +export default GrayLayer diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js new file mode 100644 index 00000000..912229c4 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/HoverTile.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Rect } from 'react-konva' +import { ROOM_HOVER_INVALID_COLOR, ROOM_HOVER_VALID_COLOR } from '../../../../util/colors' +import { TILE_SIZE_IN_PIXELS } from '../MapConstants' + +const HoverTile = ({ pixelX, pixelY, isValid, scale, onClick }) => ( + <Rect + x={pixelX} + y={pixelY} + scaleX={scale} + scaleY={scale} + width={TILE_SIZE_IN_PIXELS} + height={TILE_SIZE_IN_PIXELS} + fill={isValid ? ROOM_HOVER_VALID_COLOR : ROOM_HOVER_INVALID_COLOR} + onClick={onClick} + /> +) + +HoverTile.propTypes = { + pixelX: PropTypes.number.isRequired, + pixelY: PropTypes.number.isRequired, + isValid: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, +} + +export default HoverTile diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/ImageComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/ImageComponent.js new file mode 100644 index 00000000..2b5c569f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/ImageComponent.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Image } from 'react-konva' + +class ImageComponent extends React.Component { + static imageCaches = {} + static propTypes = { + src: PropTypes.string.isRequired, + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + opacity: PropTypes.number.isRequired, + } + + state = { + image: null, + } + + componentDidMount() { + if (ImageComponent.imageCaches[this.props.src]) { + this.setState({ image: ImageComponent.imageCaches[this.props.src] }) + return + } + + const image = new window.Image() + image.src = this.props.src + image.onload = () => { + this.setState({ image }) + ImageComponent.imageCaches[this.props.src] = image + } + } + + render() { + return ( + <Image + image={this.state.image} + x={this.props.x} + y={this.props.y} + width={this.props.width} + height={this.props.height} + opacity={this.props.opacity} + /> + ) + } +} + +export default ImageComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js new file mode 100644 index 00000000..8c573a6f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/RackFillBar.js @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group, Rect } from 'react-konva' +import { + RACK_ENERGY_BAR_BACKGROUND_COLOR, + RACK_ENERGY_BAR_FILL_COLOR, + RACK_SPACE_BAR_BACKGROUND_COLOR, + RACK_SPACE_BAR_FILL_COLOR, +} from '../../../../util/colors' +import { + OBJECT_BORDER_WIDTH_IN_PIXELS, + OBJECT_MARGIN_IN_PIXELS, + RACK_FILL_ICON_OPACITY, + RACK_FILL_ICON_WIDTH, + TILE_SIZE_IN_PIXELS, +} from '../MapConstants' +import ImageComponent from './ImageComponent' + +const RackFillBar = ({ positionX, positionY, type, fillFraction }) => { + const halfOfObjectBorderWidth = OBJECT_BORDER_WIDTH_IN_PIXELS / 2 + const x = + positionX * TILE_SIZE_IN_PIXELS + + OBJECT_MARGIN_IN_PIXELS + + (type === 'space' ? halfOfObjectBorderWidth : 0.5 * (TILE_SIZE_IN_PIXELS - 2 * OBJECT_MARGIN_IN_PIXELS)) + const startY = positionY * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS + halfOfObjectBorderWidth + const width = 0.5 * (TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2) - halfOfObjectBorderWidth + const fullHeight = TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2 - OBJECT_BORDER_WIDTH_IN_PIXELS + + const fractionHeight = fillFraction * fullHeight + const fractionY = + (positionY + 1) * TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS - halfOfObjectBorderWidth - fractionHeight + + return ( + <Group> + <Rect + x={x} + y={startY} + width={width} + height={fullHeight} + fill={type === 'space' ? RACK_SPACE_BAR_BACKGROUND_COLOR : RACK_ENERGY_BAR_BACKGROUND_COLOR} + /> + <Rect + x={x} + y={fractionY} + width={width} + height={fractionHeight} + fill={type === 'space' ? RACK_SPACE_BAR_FILL_COLOR : RACK_ENERGY_BAR_FILL_COLOR} + /> + <ImageComponent + src={'/img/topology/rack-' + type + '-icon.png'} + x={x + width * 0.5 - RACK_FILL_ICON_WIDTH * 0.5} + y={startY + fullHeight * 0.5 - RACK_FILL_ICON_WIDTH * 0.5} + width={RACK_FILL_ICON_WIDTH} + height={RACK_FILL_ICON_WIDTH} + opacity={RACK_FILL_ICON_OPACITY} + /> + </Group> + ) +} + +RackFillBar.propTypes = { + positionX: PropTypes.number.isRequired, + positionY: PropTypes.number.isRequired, + type: PropTypes.string.isRequired, + fillFraction: PropTypes.number.isRequired, +} + +export default RackFillBar diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/RoomTile.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/RoomTile.js new file mode 100644 index 00000000..43bf918e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/RoomTile.js @@ -0,0 +1,20 @@ +import React from 'react' +import { Rect } from 'react-konva' +import Shapes from '../../../../shapes/index' +import { TILE_SIZE_IN_PIXELS } from '../MapConstants' + +const RoomTile = ({ tile, color }) => ( + <Rect + x={tile.positionX * TILE_SIZE_IN_PIXELS} + y={tile.positionY * TILE_SIZE_IN_PIXELS} + width={TILE_SIZE_IN_PIXELS} + height={TILE_SIZE_IN_PIXELS} + fill={color} + /> +) + +RoomTile.propTypes = { + tile: Shapes.Tile, +} + +export default RoomTile diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/TileObject.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/TileObject.js new file mode 100644 index 00000000..9e87cc82 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/TileObject.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Rect } from 'react-konva' +import { OBJECT_BORDER_COLOR } from '../../../../util/colors' +import { OBJECT_BORDER_WIDTH_IN_PIXELS, OBJECT_MARGIN_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants' + +const TileObject = ({ positionX, positionY, color }) => ( + <Rect + x={positionX * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS} + y={positionY * TILE_SIZE_IN_PIXELS + OBJECT_MARGIN_IN_PIXELS} + width={TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2} + height={TILE_SIZE_IN_PIXELS - OBJECT_MARGIN_IN_PIXELS * 2} + fill={color} + stroke={OBJECT_BORDER_COLOR} + strokeWidth={OBJECT_BORDER_WIDTH_IN_PIXELS} + /> +) + +TileObject.propTypes = { + positionX: PropTypes.number.isRequired, + positionY: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, +} + +export default TileObject diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js new file mode 100644 index 00000000..be3a00a8 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/TilePlusIcon.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group, Line } from 'react-konva' +import { TILE_PLUS_COLOR } from '../../../../util/colors' +import { TILE_PLUS_MARGIN_IN_PIXELS, TILE_PLUS_WIDTH_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants' + +const TilePlusIcon = ({ pixelX, pixelY, mapScale }) => { + const linePoints = [ + [ + pixelX + 0.5 * TILE_SIZE_IN_PIXELS * mapScale, + pixelY + TILE_PLUS_MARGIN_IN_PIXELS * mapScale, + pixelX + 0.5 * TILE_SIZE_IN_PIXELS * mapScale, + pixelY + TILE_SIZE_IN_PIXELS * mapScale - TILE_PLUS_MARGIN_IN_PIXELS * mapScale, + ], + [ + pixelX + TILE_PLUS_MARGIN_IN_PIXELS * mapScale, + pixelY + 0.5 * TILE_SIZE_IN_PIXELS * mapScale, + pixelX + TILE_SIZE_IN_PIXELS * mapScale - TILE_PLUS_MARGIN_IN_PIXELS * mapScale, + pixelY + 0.5 * TILE_SIZE_IN_PIXELS * mapScale, + ], + ] + return ( + <Group> + {linePoints.map((points, index) => ( + <Line + key={index} + points={points} + lineCap="round" + stroke={TILE_PLUS_COLOR} + strokeWidth={TILE_PLUS_WIDTH_IN_PIXELS * mapScale} + listening={false} + /> + ))} + </Group> + ) +} + +TilePlusIcon.propTypes = { + pixelX: PropTypes.number, + pixelY: PropTypes.number, + mapScale: PropTypes.number, +} + +export default TilePlusIcon diff --git a/opendc-web/opendc-web-ui/src/components/app/map/elements/WallSegment.js b/opendc-web/opendc-web-ui/src/components/app/map/elements/WallSegment.js new file mode 100644 index 00000000..8aa2aebf --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/elements/WallSegment.js @@ -0,0 +1,32 @@ +import React from 'react' +import { Line } from 'react-konva' +import Shapes from '../../../../shapes/index' +import { WALL_COLOR } from '../../../../util/colors' +import { TILE_SIZE_IN_PIXELS, WALL_WIDTH_IN_PIXELS } from '../MapConstants' + +const WallSegment = ({ wallSegment }) => { + let points + if (wallSegment.isHorizontal) { + points = [ + wallSegment.startPosX * TILE_SIZE_IN_PIXELS, + wallSegment.startPosY * TILE_SIZE_IN_PIXELS, + (wallSegment.startPosX + wallSegment.length) * TILE_SIZE_IN_PIXELS, + wallSegment.startPosY * TILE_SIZE_IN_PIXELS, + ] + } else { + points = [ + wallSegment.startPosX * TILE_SIZE_IN_PIXELS, + wallSegment.startPosY * TILE_SIZE_IN_PIXELS, + wallSegment.startPosX * TILE_SIZE_IN_PIXELS, + (wallSegment.startPosY + wallSegment.length) * TILE_SIZE_IN_PIXELS, + ] + } + + return <Line points={points} lineCap="round" stroke={WALL_COLOR} strokeWidth={WALL_WIDTH_IN_PIXELS} /> +} + +WallSegment.propTypes = { + wallSegment: Shapes.WallSegment, +} + +export default WallSegment diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/GridGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/GridGroup.js new file mode 100644 index 00000000..ebc00244 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/GridGroup.js @@ -0,0 +1,34 @@ +import React from 'react' +import { Group, Line } from 'react-konva' +import { GRID_COLOR } from '../../../../util/colors' +import { GRID_LINE_WIDTH_IN_PIXELS, MAP_SIZE, MAP_SIZE_IN_PIXELS, TILE_SIZE_IN_PIXELS } from '../MapConstants' + +const MAP_COORDINATE_ENTRIES = Array.from(new Array(MAP_SIZE), (x, i) => i) +const HORIZONTAL_POINT_PAIRS = MAP_COORDINATE_ENTRIES.map((index) => [ + 0, + index * TILE_SIZE_IN_PIXELS, + MAP_SIZE_IN_PIXELS, + index * TILE_SIZE_IN_PIXELS, +]) +const VERTICAL_POINT_PAIRS = MAP_COORDINATE_ENTRIES.map((index) => [ + index * TILE_SIZE_IN_PIXELS, + 0, + index * TILE_SIZE_IN_PIXELS, + MAP_SIZE_IN_PIXELS, +]) + +const GridGroup = () => ( + <Group> + {HORIZONTAL_POINT_PAIRS.concat(VERTICAL_POINT_PAIRS).map((points, index) => ( + <Line + key={index} + points={points} + stroke={GRID_COLOR} + strokeWidth={GRID_LINE_WIDTH_IN_PIXELS} + listening={false} + /> + ))} + </Group> +) + +export default GridGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js new file mode 100644 index 00000000..eb6dc24a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/RackGroup.js @@ -0,0 +1,25 @@ +import React from 'react' +import { Group } from 'react-konva' +import RackEnergyFillContainer from '../../../../containers/app/map/RackEnergyFillContainer' +import RackSpaceFillContainer from '../../../../containers/app/map/RackSpaceFillContainer' +import Shapes from '../../../../shapes/index' +import { RACK_BACKGROUND_COLOR } from '../../../../util/colors' +import TileObject from '../elements/TileObject' + +const RackGroup = ({ tile }) => { + return ( + <Group> + <TileObject positionX={tile.positionX} positionY={tile.positionY} color={RACK_BACKGROUND_COLOR} /> + <Group> + <RackSpaceFillContainer tileId={tile._id} positionX={tile.positionX} positionY={tile.positionY} /> + <RackEnergyFillContainer tileId={tile._id} positionX={tile.positionX} positionY={tile.positionY} /> + </Group> + </Group> + ) +} + +RackGroup.propTypes = { + tile: Shapes.Tile, +} + +export default RackGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js new file mode 100644 index 00000000..1fd54687 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/RoomGroup.js @@ -0,0 +1,48 @@ +import React from 'react' +import { Group } from 'react-konva' +import GrayContainer from '../../../../containers/app/map/GrayContainer' +import TileContainer from '../../../../containers/app/map/TileContainer' +import WallContainer from '../../../../containers/app/map/WallContainer' +import Shapes from '../../../../shapes/index' + +const RoomGroup = ({ room, interactionLevel, currentRoomInConstruction, onClick }) => { + if (currentRoomInConstruction === room._id) { + return ( + <Group onClick={onClick}> + {room.tileIds.map((tileId) => ( + <TileContainer key={tileId} tileId={tileId} newTile={true} /> + ))} + </Group> + ) + } + + return ( + <Group onClick={onClick}> + {(() => { + if ( + (interactionLevel.mode === 'RACK' || interactionLevel.mode === 'MACHINE') && + interactionLevel.roomId === room._id + ) { + return [ + room.tileIds + .filter((tileId) => tileId !== interactionLevel.tileId) + .map((tileId) => <TileContainer key={tileId} tileId={tileId} />), + <GrayContainer key={-1} />, + room.tileIds + .filter((tileId) => tileId === interactionLevel.tileId) + .map((tileId) => <TileContainer key={tileId} tileId={tileId} />), + ] + } else { + return room.tileIds.map((tileId) => <TileContainer key={tileId} tileId={tileId} />) + } + })()} + <WallContainer roomId={room._id} /> + </Group> + ) +} + +RoomGroup.propTypes = { + room: Shapes.Room, +} + +export default RoomGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js new file mode 100644 index 00000000..1e106823 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/TileGroup.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group } from 'react-konva' +import RackContainer from '../../../../containers/app/map/RackContainer' +import Shapes from '../../../../shapes/index' +import { ROOM_DEFAULT_COLOR, ROOM_IN_CONSTRUCTION_COLOR } from '../../../../util/colors' +import RoomTile from '../elements/RoomTile' + +const TileGroup = ({ tile, newTile, roomLoad, onClick }) => { + let tileObject + if (tile.rackId) { + tileObject = <RackContainer tile={tile} /> + } else { + tileObject = null + } + + let color = ROOM_DEFAULT_COLOR + if (newTile) { + color = ROOM_IN_CONSTRUCTION_COLOR + } + + return ( + <Group onClick={() => onClick(tile)}> + <RoomTile tile={tile} color={color} /> + {tileObject} + </Group> + ) +} + +TileGroup.propTypes = { + tile: Shapes.Tile, + newTile: PropTypes.bool, +} + +export default TileGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js new file mode 100644 index 00000000..6096fc8b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/TopologyGroup.js @@ -0,0 +1,44 @@ +import React from 'react' +import { Group } from 'react-konva' +import GrayContainer from '../../../../containers/app/map/GrayContainer' +import RoomContainer from '../../../../containers/app/map/RoomContainer' +import Shapes from '../../../../shapes/index' + +const TopologyGroup = ({ topology, interactionLevel }) => { + if (!topology) { + return <Group /> + } + + if (interactionLevel.mode === 'BUILDING') { + return ( + <Group> + {topology.roomIds.map((roomId) => ( + <RoomContainer key={roomId} roomId={roomId} /> + ))} + </Group> + ) + } + + return ( + <Group> + {topology.roomIds + .filter((roomId) => roomId !== interactionLevel.roomId) + .map((roomId) => ( + <RoomContainer key={roomId} roomId={roomId} /> + ))} + {interactionLevel.mode === 'ROOM' ? <GrayContainer /> : null} + {topology.roomIds + .filter((roomId) => roomId === interactionLevel.roomId) + .map((roomId) => ( + <RoomContainer key={roomId} roomId={roomId} /> + ))} + </Group> + ) +} + +TopologyGroup.propTypes = { + topology: Shapes.Topology, + interactionLevel: Shapes.InteractionLevel, +} + +export default TopologyGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/groups/WallGroup.js b/opendc-web/opendc-web-ui/src/components/app/map/groups/WallGroup.js new file mode 100644 index 00000000..7b0f5ca0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/groups/WallGroup.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Group } from 'react-konva' +import Shapes from '../../../../shapes/index' +import { deriveWallLocations } from '../../../../util/tile-calculations' +import WallSegment from '../elements/WallSegment' + +const WallGroup = ({ tiles }) => { + return ( + <Group> + {deriveWallLocations(tiles).map((wallSegment, index) => ( + <WallSegment key={index} wallSegment={wallSegment} /> + ))} + </Group> + ) +} + +WallGroup.propTypes = { + tiles: PropTypes.arrayOf(Shapes.Tile).isRequired, +} + +export default WallGroup diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js new file mode 100644 index 00000000..bead87de --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/HoverLayerComponent.js @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Layer } from 'react-konva' +import HoverTile from '../elements/HoverTile' +import { TILE_SIZE_IN_PIXELS } from '../MapConstants' + +class HoverLayerComponent extends React.Component { + static propTypes = { + mouseX: PropTypes.number.isRequired, + mouseY: PropTypes.number.isRequired, + mapPosition: PropTypes.object.isRequired, + mapScale: PropTypes.number.isRequired, + isEnabled: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + } + + state = { + positionX: -1, + positionY: -1, + validity: false, + } + + componentDidUpdate() { + if (!this.props.isEnabled()) { + return + } + + const positionX = Math.floor( + (this.props.mouseX - this.props.mapPosition.x) / (this.props.mapScale * TILE_SIZE_IN_PIXELS) + ) + const positionY = Math.floor( + (this.props.mouseY - this.props.mapPosition.y) / (this.props.mapScale * TILE_SIZE_IN_PIXELS) + ) + + if (positionX !== this.state.positionX || positionY !== this.state.positionY) { + this.setState({ + positionX, + positionY, + validity: this.props.isValid(positionX, positionY), + }) + } + } + + render() { + if (!this.props.isEnabled()) { + return <Layer /> + } + + const pixelX = this.props.mapScale * this.state.positionX * TILE_SIZE_IN_PIXELS + this.props.mapPosition.x + const pixelY = this.props.mapScale * this.state.positionY * TILE_SIZE_IN_PIXELS + this.props.mapPosition.y + + return ( + <Layer opacity={0.6}> + <HoverTile + pixelX={pixelX} + pixelY={pixelY} + scale={this.props.mapScale} + isValid={this.state.validity} + onClick={() => + this.state.validity ? this.props.onClick(this.state.positionX, this.state.positionY) : undefined + } + /> + {this.props.children + ? React.cloneElement(this.props.children, { + pixelX, + pixelY, + scale: this.props.mapScale, + }) + : undefined} + </Layer> + ) + } +} + +export default HoverLayerComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js new file mode 100644 index 00000000..8ee14c9c --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/MapLayerComponent.js @@ -0,0 +1,17 @@ +import React from 'react' +import { Group, Layer } from 'react-konva' +import TopologyContainer from '../../../../containers/app/map/TopologyContainer' +import Backdrop from '../elements/Backdrop' +import GridGroup from '../groups/GridGroup' + +const MapLayerComponent = ({ mapPosition, mapScale }) => ( + <Layer> + <Group x={mapPosition.x} y={mapPosition.y} scaleX={mapScale} scaleY={mapScale}> + <Backdrop /> + <TopologyContainer /> + <GridGroup /> + </Group> + </Layer> +) + +export default MapLayerComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js new file mode 100644 index 00000000..661fc255 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/ObjectHoverLayerComponent.js @@ -0,0 +1,11 @@ +import React from 'react' +import TilePlusIcon from '../elements/TilePlusIcon' +import HoverLayerComponent from './HoverLayerComponent' + +const ObjectHoverLayerComponent = (props) => ( + <HoverLayerComponent {...props}> + <TilePlusIcon {...props} /> + </HoverLayerComponent> +) + +export default ObjectHoverLayerComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js new file mode 100644 index 00000000..887e2891 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/map/layers/RoomHoverLayerComponent.js @@ -0,0 +1,6 @@ +import React from 'react' +import HoverLayerComponent from './HoverLayerComponent' + +const RoomHoverLayerComponent = (props) => <HoverLayerComponent {...props} /> + +export default RoomHoverLayerComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js new file mode 100644 index 00000000..759acd57 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/results/PortfolioResultsComponent.js @@ -0,0 +1,93 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Bar, CartesianGrid, ComposedChart, ErrorBar, ResponsiveContainer, Scatter, XAxis, YAxis } from 'recharts' +import { AVAILABLE_METRICS, METRIC_NAMES_SHORT, METRIC_UNITS } from '../../../util/available-metrics' +import { mean, std } from 'mathjs' +import Shapes from '../../../shapes/index' +import approx from 'approximate-number' + +const PortfolioResultsComponent = ({ portfolio, scenarios }) => { + if (!portfolio) { + return <div>Loading...</div> + } + + const nonFinishedScenarios = scenarios.filter((s) => s.simulation.state !== 'FINISHED') + + if (nonFinishedScenarios.length > 0) { + if (nonFinishedScenarios.every((s) => s.simulation.state === 'QUEUED' || s.simulation.state === 'RUNNING')) { + return ( + <div> + <h1>Simulation running...</h1> + <p>{nonFinishedScenarios.length} of the scenarios are still being simulated</p> + </div> + ) + } + if (nonFinishedScenarios.some((s) => s.simulation.state === 'FAILED')) { + return ( + <div> + <h1>Simulation failed.</h1> + <p> + Try again by creating a new scenario. Please contact the OpenDC team for support, if issues + persist. + </p> + </div> + ) + } + } + + const dataPerMetric = {} + + AVAILABLE_METRICS.forEach((metric) => { + dataPerMetric[metric] = scenarios.map((scenario) => ({ + name: scenario.name, + value: mean(scenario.results[metric]), + errorX: std(scenario.results[metric]), + })) + }) + + return ( + <div className="full-height" style={{ overflowY: 'scroll', overflowX: 'hidden' }}> + <h2>Portfolio: {portfolio.name}</h2> + <p>Repeats per Scenario: {portfolio.targets.repeatsPerScenario}</p> + <div className="row"> + {AVAILABLE_METRICS.map((metric) => ( + <div className="col-6 mb-2" key={metric}> + <h4>{METRIC_NAMES_SHORT[metric]}</h4> + <ResponsiveContainer aspect={16 / 9} width="100%"> + <ComposedChart + data={dataPerMetric[metric]} + margin={{ left: 35, bottom: 15 }} + layout="vertical" + > + <CartesianGrid strokeDasharray="3 3" /> + <XAxis + tickFormatter={(tick) => approx(tick)} + label={{ value: METRIC_UNITS[metric], position: 'bottom', offset: 0 }} + type="number" + /> + <YAxis dataKey="name" type="category" /> + <Bar dataKey="value" fill="#3399FF" isAnimationActive={false} /> + <Scatter dataKey="value" opacity={0} isAnimationActive={false}> + <ErrorBar + dataKey="errorX" + width={10} + strokeWidth={3} + stroke="#FF6600" + direction="x" + /> + </Scatter> + </ComposedChart> + </ResponsiveContainer> + </div> + ))} + </div> + </div> + ) +} + +PortfolioResultsComponent.propTypes = { + portfolio: Shapes.Portfolio, + scenarios: PropTypes.arrayOf(Shapes.Scenario), +} + +export default PortfolioResultsComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js new file mode 100644 index 00000000..f7368f54 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.js @@ -0,0 +1,53 @@ +import PropTypes from 'prop-types' +import classNames from 'classnames' +import React from 'react' +import './Sidebar.sass' + +class Sidebar extends React.Component { + static propTypes = { + isRight: PropTypes.bool.isRequired, + collapsible: PropTypes.bool, + } + + static defaultProps = { + collapsible: true, + } + + state = { + collapsed: false, + } + + render() { + const collapseButton = ( + <div + className={classNames('sidebar-collapse-button', { + 'sidebar-collapse-button-right': this.props.isRight, + })} + onClick={() => this.setState({ collapsed: !this.state.collapsed })} + > + {(this.state.collapsed && this.props.isRight) || (!this.state.collapsed && !this.props.isRight) ? ( + <span className="fa fa-angle-left" title={this.props.isRight ? 'Expand' : 'Collapse'} /> + ) : ( + <span className="fa fa-angle-right" title={this.props.isRight ? 'Collapse' : 'Expand'} /> + )} + </div> + ) + + if (this.state.collapsed) { + return collapseButton + } + return ( + <div + className={classNames('sidebar p-3 h-100', { + 'sidebar-right': this.props.isRight, + })} + onWheel={(e) => e.stopPropagation()} + > + {this.props.children} + {this.props.collapsible && collapseButton} + </div> + ) + } +} + +export default Sidebar diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.sass b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.sass new file mode 100644 index 00000000..b8e15716 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/Sidebar.sass @@ -0,0 +1,50 @@ +@import ../../../style-globals/_variables.sass +@import ../../../style-globals/_mixins.sass + +.sidebar-collapse-button + position: absolute + left: 5px + top: 5px + padding: 5px 7px + + background: white + border: solid 1px $gray-semi-light + z-index: 99 + + +clickable + +border-radius(5px) + +transition(background, 200ms) + + &.sidebar-collapse-button-right + left: auto + right: 5px + top: 5px + + &:hover + background: #eeeeee + +.sidebar + position: absolute + top: 0 + left: 0 + width: $side-bar-width + + z-index: 100 + background: white + + border-right: $gray-semi-dark 1px solid + + .sidebar-collapse-button + left: auto + right: -25px + +.sidebar-right + left: auto + right: 0 + + border-left: $gray-semi-dark 1px solid + border-right: none + + .sidebar-collapse-button-right + left: -25px + right: auto diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js new file mode 100644 index 00000000..b000b9e2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/PortfolioListComponent.js @@ -0,0 +1,66 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Shapes from '../../../../shapes' +import { Link } from 'react-router-dom' +import FontAwesome from 'react-fontawesome' +import ScenarioListContainer from '../../../../containers/app/sidebars/project/ScenarioListContainer' + +class PortfolioListComponent extends React.Component { + static propTypes = { + portfolios: PropTypes.arrayOf(Shapes.Portfolio), + currentProjectId: PropTypes.string.isRequired, + currentPortfolioId: PropTypes.string, + onNewPortfolio: PropTypes.func.isRequired, + onChoosePortfolio: PropTypes.func.isRequired, + onDeletePortfolio: PropTypes.func.isRequired, + } + + onDelete(id) { + this.props.onDeletePortfolio(id) + } + + render() { + return ( + <div className="pb-3"> + <h2> + Portfolios + <button + className="btn btn-outline-primary float-right" + onClick={this.props.onNewPortfolio.bind(this)} + > + <FontAwesome name="plus" /> + </button> + </h2> + + {this.props.portfolios.map((portfolio, idx) => ( + <div key={portfolio._id}> + <div className="row mb-1"> + <div + className={ + 'col-7 align-self-center ' + + (portfolio._id === this.props.currentPortfolioId ? 'font-weight-bold' : '') + } + > + {portfolio.name} + </div> + <div className="col-5 text-right"> + <Link + className="btn btn-outline-primary mr-1 fa fa-play" + to={`/projects/${this.props.currentProjectId}/portfolios/${portfolio._id}`} + onClick={() => this.props.onChoosePortfolio(portfolio._id)} + /> + <span + className="btn btn-outline-danger fa fa-trash" + onClick={() => this.onDelete(portfolio._id)} + /> + </div> + </div> + <ScenarioListContainer portfolioId={portfolio._id} /> + </div> + ))} + </div> + ) + } +} + +export default PortfolioListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js new file mode 100644 index 00000000..4789315e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ProjectSidebarComponent.js @@ -0,0 +1,15 @@ +import React from 'react' +import Sidebar from '../Sidebar' +import TopologyListContainer from '../../../../containers/app/sidebars/project/TopologyListContainer' +import PortfolioListContainer from '../../../../containers/app/sidebars/project/PortfolioListContainer' + +const ProjectSidebarComponent = ({ collapsible }) => ( + <Sidebar isRight={false} collapsible={collapsible}> + <div className="h-100 overflow-auto container-fluid"> + <TopologyListContainer /> + <PortfolioListContainer /> + </div> + </Sidebar> +) + +export default ProjectSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js new file mode 100644 index 00000000..e775a663 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/ScenarioListComponent.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Shapes from '../../../../shapes' +import { Link } from 'react-router-dom' +import FontAwesome from 'react-fontawesome' + +class ScenarioListComponent extends React.Component { + static propTypes = { + scenarios: PropTypes.arrayOf(Shapes.Scenario), + portfolioId: PropTypes.string, + currentProjectId: PropTypes.string.isRequired, + currentScenarioId: PropTypes.string, + onNewScenario: PropTypes.func.isRequired, + onChooseScenario: PropTypes.func.isRequired, + onDeleteScenario: PropTypes.func.isRequired, + } + + onDelete(id) { + this.props.onDeleteScenario(id) + } + + render() { + return ( + <> + {this.props.scenarios.map((scenario, idx) => ( + <div key={scenario._id} className="row mb-1"> + <div + className={ + 'col-7 pl-5 align-self-center ' + + (scenario._id === this.props.currentScenarioId ? 'font-weight-bold' : '') + } + > + {scenario.name} + </div> + <div className="col-5 text-right"> + <Link + className="btn btn-outline-primary mr-1 fa fa-play disabled" + to={`/projects/${this.props.currentProjectId}/portfolios/${scenario.portfolioId}/scenarios/${scenario._id}`} + onClick={() => this.props.onChooseScenario(scenario.portfolioId, scenario._id)} + /> + <span + className={'btn btn-outline-danger fa fa-trash ' + (idx === 0 ? 'disabled' : '')} + onClick={() => (idx !== 0 ? this.onDelete(scenario._id) : undefined)} + /> + </div> + </div> + ))} + <div className="pl-4 mb-2"> + <div + className="btn btn-outline-primary" + onClick={() => this.props.onNewScenario(this.props.portfolioId)} + > + <FontAwesome name="plus" className="mr-1" /> + New scenario + </div> + </div> + </> + ) + } +} + +export default ScenarioListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js new file mode 100644 index 00000000..2f42f7e4 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/project/TopologyListComponent.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Shapes from '../../../../shapes' +import FontAwesome from 'react-fontawesome' + +class TopologyListComponent extends React.Component { + static propTypes = { + topologies: PropTypes.arrayOf(Shapes.Topology), + currentTopologyId: PropTypes.string, + onChooseTopology: PropTypes.func.isRequired, + onNewTopology: PropTypes.func.isRequired, + onDeleteTopology: PropTypes.func.isRequired, + } + + onChoose(id) { + this.props.onChooseTopology(id) + } + + onDelete(id) { + this.props.onDeleteTopology(id) + } + + render() { + return ( + <div className="pb-3"> + <h2> + Topologies + <button className="btn btn-outline-primary float-right" onClick={this.props.onNewTopology}> + <FontAwesome name="plus" /> + </button> + </h2> + + {this.props.topologies.map((topology, idx) => ( + <div key={topology._id} className="row mb-1"> + <div + className={ + 'col-7 align-self-center ' + + (topology._id === this.props.currentTopologyId ? 'font-weight-bold' : '') + } + > + {topology.name} + </div> + <div className="col-5 text-right"> + <span + className="btn btn-outline-primary mr-1 fa fa-play" + onClick={() => this.onChoose(topology._id)} + /> + <span + className={'btn btn-outline-danger fa fa-trash ' + (idx === 0 ? 'disabled' : '')} + onClick={() => (idx !== 0 ? this.onDelete(topology._id) : undefined)} + /> + </div> + </div> + ))} + </div> + ) + } +} + +export default TopologyListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js new file mode 100644 index 00000000..5fb0dc55 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/NameComponent.js @@ -0,0 +1,13 @@ +import React from 'react' +import FontAwesome from 'react-fontawesome' + +const NameComponent = ({ name, onEdit }) => ( + <h2> + {name} + <button className="btn btn-outline-secondary float-right" onClick={onEdit}> + <FontAwesome name="pencil" /> + </button> + </h2> +) + +export default NameComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js new file mode 100644 index 00000000..f5eee36b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/TopologySidebarComponent.js @@ -0,0 +1,31 @@ +import React from 'react' +import BuildingSidebarContainer from '../../../../containers/app/sidebars/topology/building/BuildingSidebarContainer' +import MachineSidebarContainer from '../../../../containers/app/sidebars/topology/machine/MachineSidebarContainer' +import RackSidebarContainer from '../../../../containers/app/sidebars/topology/rack/RackSidebarContainer' +import RoomSidebarContainer from '../../../../containers/app/sidebars/topology/room/RoomSidebarContainer' +import Sidebar from '../Sidebar' + +const TopologySidebarComponent = ({ interactionLevel }) => { + let sidebarContent + + switch (interactionLevel.mode) { + case 'BUILDING': + sidebarContent = <BuildingSidebarContainer /> + break + case 'ROOM': + sidebarContent = <RoomSidebarContainer /> + break + case 'RACK': + sidebarContent = <RackSidebarContainer /> + break + case 'MACHINE': + sidebarContent = <MachineSidebarContainer /> + break + default: + sidebarContent = 'Missing Content' + } + + return <Sidebar isRight={true}>{sidebarContent}</Sidebar> +} + +export default TopologySidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js new file mode 100644 index 00000000..eea62f84 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/BuildingSidebarComponent.js @@ -0,0 +1,13 @@ +import React from 'react' +import NewRoomConstructionContainer from '../../../../../containers/app/sidebars/topology/building/NewRoomConstructionContainer' + +const BuildingSidebarComponent = () => { + return ( + <div> + <h2>Building</h2> + <NewRoomConstructionContainer /> + </div> + ) +} + +export default BuildingSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js new file mode 100644 index 00000000..fd552c1e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/building/NewRoomConstructionComponent.js @@ -0,0 +1,26 @@ +import React from 'react' + +const NewRoomConstructionComponent = ({ onStart, onFinish, onCancel, currentRoomInConstruction }) => { + if (currentRoomInConstruction === '-1') { + return ( + <div className="btn btn-outline-primary btn-block" onClick={onStart}> + <span className="fa fa-plus mr-2" /> + Construct a new room + </div> + ) + } + return ( + <div> + <div className="btn btn-primary btn-block" onClick={onFinish}> + <span className="fa fa-check mr-2" /> + Finalize new room + </div> + <div className="btn btn-default btn-block" onClick={onCancel}> + <span className="fa fa-times mr-2" /> + Cancel construction + </div> + </div> + ) +} + +export default NewRoomConstructionComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js new file mode 100644 index 00000000..70d522b2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/BackToRackComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const BackToRackComponent = ({ onClick }) => ( + <div className="btn btn-secondary btn-block" onClick={onClick}> + <span className="fa fa-angle-left mr-2" /> + Back to rack + </div> +) + +export default BackToRackComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js new file mode 100644 index 00000000..37820316 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/DeleteMachineComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const DeleteMachineComponent = ({ onClick }) => ( + <div className="btn btn-outline-danger btn-block" onClick={onClick}> + <span className="fa fa-trash mr-2" /> + Delete this machine + </div> +) + +export default DeleteMachineComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineNameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineNameComponent.js new file mode 100644 index 00000000..992383c4 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineNameComponent.js @@ -0,0 +1,5 @@ +import React from 'react' + +const MachineNameComponent = ({ position }) => <h2>Machine at slot {position}</h2> + +export default MachineNameComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js new file mode 100644 index 00000000..7c78cf9e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/MachineSidebarComponent.js @@ -0,0 +1,18 @@ +import React from 'react' +import BackToRackContainer from '../../../../../containers/app/sidebars/topology/machine/BackToRackContainer' +import DeleteMachineContainer from '../../../../../containers/app/sidebars/topology/machine/DeleteMachineContainer' +import MachineNameContainer from '../../../../../containers/app/sidebars/topology/machine/MachineNameContainer' +import UnitTabsContainer from '../../../../../containers/app/sidebars/topology/machine/UnitTabsContainer' + +const MachineSidebarComponent = ({ machineId }) => { + return ( + <div className="h-100 overflow-auto"> + <MachineNameContainer /> + <BackToRackContainer /> + <DeleteMachineContainer /> + <UnitTabsContainer /> + </div> + ) +} + +export default MachineSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js new file mode 100644 index 00000000..4e9dbc7e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitAddComponent.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types' +import React from 'react' + +class UnitAddComponent extends React.Component { + static propTypes = { + units: PropTypes.array.isRequired, + onAdd: PropTypes.func.isRequired, + } + + render() { + return ( + <div className="form-inline"> + <div className="form-group w-100"> + <select className="form-control w-70 mr-1" ref={(unitSelect) => (this.unitSelect = unitSelect)}> + {this.props.units.map((unit) => ( + <option value={unit._id} key={unit._id}> + {unit.name} + </option> + ))} + </select> + <button + type="submit" + className="btn btn-outline-primary" + onClick={() => this.props.onAdd(this.unitSelect.value)} + > + <span className="fa fa-plus mr-2" /> + Add + </button> + </div> + </div> + ) + } +} + +export default UnitAddComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js new file mode 100644 index 00000000..de55e506 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitComponent.js @@ -0,0 +1,52 @@ +import React from 'react' +import { UncontrolledPopover, PopoverHeader, PopoverBody, Button } from 'reactstrap' + +function UnitComponent({ index, unitType, unit, onDelete }) { + let unitInfo + if (unitType === 'cpu' || unitType === 'gpu') { + unitInfo = ( + <> + <strong>Clockrate: </strong> + <code>{unit.clockRateMhz}</code> + <br /> + <strong>Num. Cores: </strong> + <code>{unit.numberOfCores}</code> + <br /> + <strong>Energy Cons.: </strong> + <code>{unit.energyConsumptionW} W</code> + <br /> + </> + ) + } else if (unitType === 'memory' || unitType === 'storage') { + unitInfo = ( + <> + <strong>Speed:</strong> + <code>{unit.speedMbPerS} Mb/s</code> + <br /> + <strong>Size:</strong> + <code>{unit.sizeMb} MB</code> + <br /> + <strong>Energy Cons.:</strong> + <code>{unit.energyConsumptionW} W</code> + <br /> + </> + ) + } + + return ( + <li className="d-flex list-group-item justify-content-between align-items-center"> + <span style={{ maxWidth: '60%' }}>{unit.name}</span> + <span> + <Button outline={true} color="info" className="mr-1 fa fa-info-circle" id={`unit-${index}`} /> + <UncontrolledPopover trigger="focus" placement="left" target={`unit-${index}`}> + <PopoverHeader>Unit Information</PopoverHeader> + <PopoverBody>{unitInfo}</PopoverBody> + </UncontrolledPopover> + + <span className="btn btn-outline-danger fa fa-trash" onClick={onDelete} /> + </span> + </li> + ) +} + +export default UnitComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js new file mode 100644 index 00000000..2ade0f6a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitListComponent.js @@ -0,0 +1,20 @@ +import React from 'react' +import UnitContainer from '../../../../../containers/app/sidebars/topology/machine/UnitContainer' + +const UnitListComponent = ({ unitType, unitIds }) => ( + <ul className="list-group mt-1"> + {unitIds.length !== 0 ? ( + unitIds.map((unitId, index) => ( + <UnitContainer unitType={unitType} unitId={unitId} index={index} key={index} /> + )) + ) : ( + <div className="alert alert-info"> + <span> + <strong>No units...</strong> Add some with the menu above! + </span> + </div> + )} + </ul> +) + +export default UnitListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js new file mode 100644 index 00000000..6599fefd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/machine/UnitTabsComponent.js @@ -0,0 +1,78 @@ +import React, { useState } from 'react' +import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap' +import UnitAddContainer from '../../../../../containers/app/sidebars/topology/machine/UnitAddContainer' +import UnitListContainer from '../../../../../containers/app/sidebars/topology/machine/UnitListContainer' + +const UnitTabsComponent = () => { + const [activeTab, setActiveTab] = useState('cpu-units') + const toggle = (tab) => { + if (activeTab !== tab) setActiveTab(tab) + } + + return ( + <div> + <Nav tabs> + <NavItem> + <NavLink + className={activeTab === 'cpu-units' ? 'active' : ''} + onClick={() => { + toggle('cpu-units') + }} + > + CPU + </NavLink> + </NavItem> + <NavItem> + <NavLink + className={activeTab === 'gpu-units' ? 'active' : ''} + onClick={() => { + toggle('gpu-units') + }} + > + GPU + </NavLink> + </NavItem> + <NavItem> + <NavLink + className={activeTab === 'memory-units' ? 'active' : ''} + onClick={() => { + toggle('memory-units') + }} + > + Memory + </NavLink> + </NavItem> + <NavItem> + <NavLink + className={activeTab === 'storage-units' ? 'active' : ''} + onClick={() => { + toggle('storage-units') + }} + > + Stor. + </NavLink> + </NavItem> + </Nav> + <TabContent activeTab={activeTab}> + <TabPane tabId="cpu-units"> + <UnitAddContainer unitType="cpu" /> + <UnitListContainer unitType="cpu" /> + </TabPane> + <TabPane tabId="gpu-units"> + <UnitAddContainer unitType="gpu" /> + <UnitListContainer unitType="gpu" /> + </TabPane> + <TabPane tabId="memory-units"> + <UnitAddContainer unitType="memory" /> + <UnitListContainer unitType="memory" /> + </TabPane> + <TabPane tabId="storage-units"> + <UnitAddContainer unitType="storage" /> + <UnitListContainer unitType="storage" /> + </TabPane> + </TabContent> + </div> + ) +} + +export default UnitTabsComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js new file mode 100644 index 00000000..75418f9d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/AddPrefabComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const AddPrefabComponent = ({ onClick }) => ( + <div className="btn btn-primary btn-block" onClick={onClick}> + <span className="fa fa-floppy-o mr-2" /> + Save this rack to a prefab + </div> +) + +export default AddPrefabComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js new file mode 100644 index 00000000..c14775bf --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/BackToRoomComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const BackToRoomComponent = ({ onClick }) => ( + <div className="btn btn-secondary btn-block mb-2" onClick={onClick}> + <span className="fa fa-angle-left mr-2" /> + Back to room + </div> +) + +export default BackToRoomComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackComponent.js new file mode 100644 index 00000000..23b0daac --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/DeleteRackComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const DeleteRackComponent = ({ onClick }) => ( + <div className="btn btn-outline-danger btn-block" onClick={onClick}> + <span className="fa fa-trash mr-2" /> + Delete this rack + </div> +) + +export default DeleteRackComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js new file mode 100644 index 00000000..d7e30f1d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/EmptySlotComponent.js @@ -0,0 +1,13 @@ +import React from 'react' + +const EmptySlotComponent = ({ position, onAdd }) => ( + <li className="list-group-item d-flex justify-content-between align-items-center"> + <span className="badge badge-default badge-info mr-1 disabled">{position}</span> + <button className="btn btn-outline-primary" onClick={onAdd}> + <span className="fa fa-plus mr-2" /> + Add machine + </button> + </li> +) + +export default EmptySlotComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js new file mode 100644 index 00000000..caa3dc04 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineComponent.js @@ -0,0 +1,43 @@ +import React from 'react' +import Shapes from '../../../../../shapes' + +const UnitIcon = ({ id, type }) => ( + <div> + <img + src={'/img/topology/' + id + '-icon.png'} + alt={'Machine contains ' + type + ' units'} + className="img-fluid ml-1" + style={{ maxHeight: '35px' }} + /> + </div> +) + +const MachineComponent = ({ position, machine, onClick }) => { + const hasNoUnits = + machine.cpuIds.length + machine.gpuIds.length + machine.memoryIds.length + machine.storageIds.length === 0 + + return ( + <li + className="d-flex list-group-item list-group-item-action justify-content-between align-items-center" + onClick={onClick} + style={{ backgroundColor: 'white' }} + > + <span className="badge badge-default badge-info mr-1">{position}</span> + <div className="d-inline-flex"> + {machine.cpuIds.length > 0 ? <UnitIcon id="cpu" type="CPU" /> : undefined} + {machine.gpuIds.length > 0 ? <UnitIcon id="gpu" type="GPU" /> : undefined} + {machine.memoryIds.length > 0 ? <UnitIcon id="memory" type="memory" /> : undefined} + {machine.storageIds.length > 0 ? <UnitIcon id="storage" type="storage" /> : undefined} + {hasNoUnits ? ( + <span className="badge badge-default badge-warning">Machine with no units</span> + ) : undefined} + </div> + </li> + ) +} + +MachineComponent.propTypes = { + machine: Shapes.Machine, +} + +export default MachineComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js new file mode 100644 index 00000000..12be26bd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.js @@ -0,0 +1,20 @@ +import React from 'react' +import EmptySlotContainer from '../../../../../containers/app/sidebars/topology/rack/EmptySlotContainer' +import MachineContainer from '../../../../../containers/app/sidebars/topology/rack/MachineContainer' +import './MachineListComponent.sass' + +const MachineListComponent = ({ machineIds }) => { + return ( + <ul className="list-group machine-list"> + {machineIds.map((machineId, index) => { + if (machineId === null) { + return <EmptySlotContainer key={index} position={index + 1} /> + } else { + return <MachineContainer key={index} position={index + 1} machineId={machineId} /> + } + })} + </ul> + ) +} + +export default MachineListComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.sass b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.sass new file mode 100644 index 00000000..11b82c93 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/MachineListComponent.sass @@ -0,0 +1,2 @@ +.machine-list li + min-height: 64px diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameComponent.js new file mode 100644 index 00000000..b701909a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackNameComponent.js @@ -0,0 +1,6 @@ +import React from 'react' +import NameComponent from '../NameComponent' + +const RackNameComponent = ({ rackName, onEdit }) => <NameComponent name={rackName} onEdit={onEdit} /> + +export default RackNameComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js new file mode 100644 index 00000000..ca41bf57 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.js @@ -0,0 +1,25 @@ +import React from 'react' +import BackToRoomContainer from '../../../../../containers/app/sidebars/topology/rack/BackToRoomContainer' +import DeleteRackContainer from '../../../../../containers/app/sidebars/topology/rack/DeleteRackContainer' +import MachineListContainer from '../../../../../containers/app/sidebars/topology/rack/MachineListContainer' +import RackNameContainer from '../../../../../containers/app/sidebars/topology/rack/RackNameContainer' +import './RackSidebarComponent.sass' +import AddPrefabContainer from '../../../../../containers/app/sidebars/topology/rack/AddPrefabContainer' + +const RackSidebarComponent = () => { + return ( + <div className="rack-sidebar-container flex-column"> + <div className="rack-sidebar-header-container"> + <RackNameContainer /> + <BackToRoomContainer /> + <AddPrefabContainer /> + <DeleteRackContainer /> + </div> + <div className="machine-list-container mt-2"> + <MachineListContainer /> + </div> + </div> + ) +} + +export default RackSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass new file mode 100644 index 00000000..29fec02a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/rack/RackSidebarComponent.sass @@ -0,0 +1,11 @@ +.rack-sidebar-container + display: flex + height: 100% + max-height: 100% + +.rack-sidebar-header-container + flex: 0 + +.machine-list-container + flex: 1 + overflow-y: scroll diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js new file mode 100644 index 00000000..64c0a1f6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/BackToBuildingComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const BackToBuildingComponent = ({ onClick }) => ( + <div className="btn btn-secondary btn-block mb-2" onClick={onClick}> + <span className="fa fa-angle-left mr-2" /> + Back to building + </div> +) + +export default BackToBuildingComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js new file mode 100644 index 00000000..78417359 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/DeleteRoomComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +const DeleteRoomComponent = ({ onClick }) => ( + <div className="btn btn-outline-danger btn-block" onClick={onClick}> + <span className="fa fa-trash mr-2" /> + Delete this room + </div> +) + +export default DeleteRoomComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomComponent.js new file mode 100644 index 00000000..857a646f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/EditRoomComponent.js @@ -0,0 +1,22 @@ +import classNames from 'classnames' +import React from 'react' + +const EditRoomComponent = ({ onEdit, onFinish, isEditing, isInRackConstructionMode }) => + isEditing ? ( + <div className="btn btn-info btn-block" onClick={onFinish}> + <span className="fa fa-check mr-2" /> + Finish editing room + </div> + ) : ( + <div + className={classNames('btn btn-outline-info btn-block', { + disabled: isInRackConstructionMode, + })} + onClick={() => (isInRackConstructionMode ? undefined : onEdit())} + > + <span className="fa fa-pencil mr-2" /> + Edit the tiles of this room + </div> + ) + +export default EditRoomComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js new file mode 100644 index 00000000..44566f61 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RackConstructionComponent.js @@ -0,0 +1,27 @@ +import classNames from 'classnames' +import React from 'react' + +const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, isEditingRoom }) => { + if (inRackConstructionMode) { + return ( + <div className="btn btn-primary btn-block" onClick={onStop}> + <span className="fa fa-times mr-2" /> + Stop rack construction + </div> + ) + } + + return ( + <div + className={classNames('btn btn-outline-primary btn-block', { + disabled: isEditingRoom, + })} + onClick={() => (isEditingRoom ? undefined : onStart())} + > + <span className="fa fa-plus mr-2" /> + Start rack construction + </div> + ) +} + +export default RackConstructionComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomNameComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomNameComponent.js new file mode 100644 index 00000000..d637828e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomNameComponent.js @@ -0,0 +1,6 @@ +import React from 'react' +import NameComponent from '../NameComponent' + +const RoomNameComponent = ({ roomName, onEdit }) => <NameComponent name={roomName} onEdit={onEdit} /> + +export default RoomNameComponent diff --git a/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js new file mode 100644 index 00000000..1bc6533e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/app/sidebars/topology/room/RoomSidebarComponent.js @@ -0,0 +1,20 @@ +import React from 'react' +import BackToBuildingContainer from '../../../../../containers/app/sidebars/topology/room/BackToBuildingContainer' +import DeleteRoomContainer from '../../../../../containers/app/sidebars/topology/room/DeleteRoomContainer' +import EditRoomContainer from '../../../../../containers/app/sidebars/topology/room/EditRoomContainer' +import RackConstructionContainer from '../../../../../containers/app/sidebars/topology/room/RackConstructionContainer' +import RoomNameContainer from '../../../../../containers/app/sidebars/topology/room/RoomNameContainer' + +const RoomSidebarComponent = () => { + return ( + <div> + <RoomNameContainer /> + <BackToBuildingContainer /> + <RackConstructionContainer /> + <EditRoomContainer /> + <DeleteRoomContainer /> + </div> + ) +} + +export default RoomSidebarComponent diff --git a/opendc-web/opendc-web-ui/src/components/home/ContactSection.js b/opendc-web/opendc-web-ui/src/components/home/ContactSection.js new file mode 100644 index 00000000..42bdab8a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/ContactSection.js @@ -0,0 +1,54 @@ +import React from 'react' +import FontAwesome from 'react-fontawesome' +import './ContactSection.sass' +import ContentSection from './ContentSection' + +const ContactSection = () => ( + <ContentSection name="contact" title="Contact"> + <div className="row justify-content-center"> + <div className="col-4"> + <a href="https://github.com/atlarge-research/opendc"> + <FontAwesome name="github" size="3x" className="mb-2" /> + <div className="w-100" /> + atlarge-research/opendc + </a> + </div> + <div className="col-4"> + <a href="mailto:opendc@atlarge-research.com"> + <FontAwesome name="envelope" size="3x" className="mb-2" /> + <div className="w-100" /> + opendc@atlarge-research.com + </a> + </div> + </div> + <div className="row"> + <div className="col text-center"> + <img src="img/tudelft-icon.png" className="img-fluid tudelft-icon" alt="TU Delft" /> + </div> + </div> + <div className="row"> + <div className="col text-center"> + A project by the + <a href="http://atlarge.science" target="_blank" rel="noopener noreferrer"> + <strong>@Large Research Group</strong> + </a> + . + </div> + </div> + <div className="row"> + <div className="col text-center disclaimer mt-5 small"> + <FontAwesome name="exclamation-triangle" size="2x" className="mr-2" /> + <br /> + OpenDC is an experimental tool. Your data may get lost, overwritten, or otherwise become unavailable. + <br /> + The OpenDC authors should in no way be liable in the event this happens (see our{' '} + <strong> + <a href="https://github.com/atlarge-research/opendc/blob/master/LICENSE.md">license</a> + </strong> + ). Sorry for the inconvenience. + </div> + </div> + </ContentSection> +) + +export default ContactSection diff --git a/opendc-web/opendc-web-ui/src/components/home/ContactSection.sass b/opendc-web/opendc-web-ui/src/components/home/ContactSection.sass new file mode 100644 index 00000000..997f8d98 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/ContactSection.sass @@ -0,0 +1,15 @@ +.contact-section + background-color: #444 + color: #ddd + + a + color: #ddd + + a:hover + color: #fff + + .tudelft-icon + height: 100px + + .disclaimer + color: #cccccc diff --git a/opendc-web/opendc-web-ui/src/components/home/ContentSection.js b/opendc-web/opendc-web-ui/src/components/home/ContentSection.js new file mode 100644 index 00000000..9d4832d9 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/ContentSection.js @@ -0,0 +1,19 @@ +import classNames from 'classnames' +import PropTypes from 'prop-types' +import React from 'react' +import './ContentSection.sass' + +const ContentSection = ({ name, title, children }) => ( + <div id={name} className={classNames(name + '-section', 'content-section')}> + <div className="container"> + <h1>{title}</h1> + {children} + </div> + </div> +) + +ContentSection.propTypes = { + name: PropTypes.string.isRequired, +} + +export default ContentSection diff --git a/opendc-web/opendc-web-ui/src/components/home/ContentSection.sass b/opendc-web/opendc-web-ui/src/components/home/ContentSection.sass new file mode 100644 index 00000000..a4c8bd66 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/ContentSection.sass @@ -0,0 +1,9 @@ +@import ../../style-globals/_variables.sass + +.content-section + padding-top: 50px + padding-bottom: 150px + text-align: center + + h1 + margin-bottom: 30px diff --git a/opendc-web/opendc-web-ui/src/components/home/IntroSection.js b/opendc-web/opendc-web-ui/src/components/home/IntroSection.js new file mode 100644 index 00000000..a799272a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/IntroSection.js @@ -0,0 +1,40 @@ +import React from 'react' + +const IntroSection = () => ( + <section id="intro" className="intro-section"> + <div className="container pt-5 pb-3"> + <div className="row justify-content-center"> + <div className="col-xl-4 col-lg-4 col-md-4 col-sm-8 col-8"> + <h4>The datacenter (DC) industry...</h4> + <ul> + <li>Is worth over $15 bn, and growing</li> + <li>Has many hard-to-grasp concepts</li> + <li>Needs to become accessible to many</li> + </ul> + </div> + <div className="col-xl-4 col-lg-4 col-md-4 col-sm-8 col-8"> + <img + src="img/datacenter-drawing.png" + className="col-12 img-fluid" + alt="Schematic top-down view of a datacenter" + /> + <p className="col-12 figure-caption text-center"> + <a href="http://www.dolphinhosts.co.uk/wp-content/uploads/2013/07/data-centers.gif"> + Image source + </a> + </p> + </div> + <div className="col-xl-4 col-lg-4 col-md-4 col-sm-8 col-8"> + <h4>OpenDC provides...</h4> + <ul> + <li>Collaborative online DC modeling</li> + <li>Diverse and effective DC simulation</li> + <li>Exploratory DC performance feedback</li> + </ul> + </div> + </div> + </div> + </section> +) + +export default IntroSection diff --git a/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.js b/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.js new file mode 100644 index 00000000..7b410679 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.js @@ -0,0 +1,18 @@ +import React from 'react' +import './JumbotronHeader.sass' + +const JumbotronHeader = () => ( + <section className="jumbotron-header"> + <div className="container"> + <div className="jumbotron text-center"> + <h1> + Open<span className="dc">DC</span> + </h1> + <p className="lead">Collaborative Datacenter Simulation and Exploration for Everybody</p> + <img src="img/logo.png" className="img-responsive mt-3" alt="OpenDC" /> + </div> + </div> + </section> +) + +export default JumbotronHeader diff --git a/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.sass b/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.sass new file mode 100644 index 00000000..1b6a89fd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/JumbotronHeader.sass @@ -0,0 +1,24 @@ +.jumbotron-header + background: #00A6D6 + +.jumbotron + background-color: inherit + margin-bottom: 0 + + padding-top: 120px + padding-bottom: 120px + + img + max-width: 110px + + h1 + color: #fff + font-size: 4.5em + + .dc + color: #fff + font-weight: bold + + .lead + color: #fff + font-size: 1.4em diff --git a/opendc-web/opendc-web-ui/src/components/home/ModelingSection.js b/opendc-web/opendc-web-ui/src/components/home/ModelingSection.js new file mode 100644 index 00000000..643dca65 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/ModelingSection.js @@ -0,0 +1,22 @@ +import React from 'react' +import ScreenshotSection from './ScreenshotSection' + +const ModelingSection = () => ( + <ScreenshotSection + name="modeling" + title="Datacenter Modeling" + imageUrl="/img/screenshot-construction.png" + caption="Building a datacenter in OpenDC" + imageIsRight={true} + > + <h3>Collaboratively...</h3> + <ul> + <li>Model DC layout, and room locations and types</li> + <li>Place racks in rooms and nodes in racks</li> + <li>Add real-world CPU, GPU, memory, storage and network units to each node</li> + <li>Select from diverse scheduling policies</li> + </ul> + </ScreenshotSection> +) + +export default ModelingSection diff --git a/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.js b/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.js new file mode 100644 index 00000000..c987d5d0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.js @@ -0,0 +1,24 @@ +import classNames from 'classnames' +import React from 'react' +import ContentSection from './ContentSection' +import './ScreenshotSection.sass' + +const ScreenshotSection = ({ name, title, imageUrl, caption, imageIsRight, children }) => ( + <ContentSection name={name} title={title}> + <div className="row"> + <div + className={classNames('col-xl-5 col-lg-5 col-md-5 col-sm-12 col-12 text-left', { + 'order-1': !imageIsRight, + })} + > + {children} + </div> + <div className="col-xl-7 col-lg-7 col-md-7 col-sm-12 col-12"> + <img src={imageUrl} className="col-12 screenshot" alt={caption} /> + <div className="row text-muted justify-content-center">{caption}</div> + </div> + </div> + </ContentSection> +) + +export default ScreenshotSection diff --git a/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.sass b/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.sass new file mode 100644 index 00000000..2f454cb4 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/ScreenshotSection.sass @@ -0,0 +1,5 @@ +.screenshot + outline: 2px black solid + padding-left: 0 + padding-right: 0 + margin-bottom: 5px diff --git a/opendc-web/opendc-web-ui/src/components/home/SimulationSection.js b/opendc-web/opendc-web-ui/src/components/home/SimulationSection.js new file mode 100644 index 00000000..b0244cb5 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/SimulationSection.js @@ -0,0 +1,22 @@ +import React from 'react' +import ScreenshotSection from './ScreenshotSection' + +const ModelingSection = () => ( + <ScreenshotSection + name="project" + title="Datacenter Simulation" + imageUrl="/img/screenshot-simulation-zoom.png" + caption="Running an experiment in OpenDC" + imageIsRight={false} + > + <h3>Working with OpenDC:</h3> + <ul> + <li>Seamlessly switch between construction and simulation modes</li> + <li>Choose one of several predefined workloads (Big Data, Bag of Tasks, Hadoop, etc.)</li> + <li>Play, pause, and skip around the informative simulation timeline</li> + <li>Visualize and demo live</li> + </ul> + </ScreenshotSection> +) + +export default ModelingSection diff --git a/opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js b/opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js new file mode 100644 index 00000000..e5ed9683 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/StakeholderSection.js @@ -0,0 +1,30 @@ +import React from 'react' +import ContentSection from './ContentSection' + +const Stakeholder = ({ name, title, subtitle }) => ( + <div className="col-xl-4 col-lg-4 col-md-4 col-sm-6 col-6"> + <img + src={'img/stakeholders/' + name + '.png'} + className="col-xl-3 col-lg-4 col-md-4 col-sm-4 col-4 img-fluid" + alt={title} + /> + <div className="text-center mt-2"> + <h4>{title}</h4> + <p>{subtitle}</p> + </div> + </div> +) + +const StakeholderSection = () => ( + <ContentSection name="stakeholders" title="Stakeholders"> + <div className="row justify-content-center"> + <Stakeholder name="Manager" title="Managers" subtitle="Seeing is deciding" /> + <Stakeholder name="Sales" title="Sales" subtitle="Demo concepts" /> + <Stakeholder name="Developer" title="DevOps" subtitle="Develop & tune" /> + <Stakeholder name="Researcher" title="Researchers" subtitle="Understand & design" /> + <Stakeholder name="Student" title="Students" subtitle="Grasp complex concepts" /> + </div> + </ContentSection> +) + +export default StakeholderSection diff --git a/opendc-web/opendc-web-ui/src/components/home/TeamSection.js b/opendc-web/opendc-web-ui/src/components/home/TeamSection.js new file mode 100644 index 00000000..4b6f1e25 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/TeamSection.js @@ -0,0 +1,53 @@ +import React from 'react' +import ContentSection from './ContentSection' + +const TeamMember = ({ photoId, name, description }) => ( + <div className="col-xl-4 col-lg-4 col-md-5 col-sm-6 col-12 justify-content-center"> + <img + src={'img/portraits/' + photoId + '.png'} + className="col-xl-10 col-lg-10 col-md-10 col-sm-8 col-5 mb-2 mt-2" + alt={name} + /> + <div className="col-12"> + <h4>{name}</h4> + <div className="team-member-description">{description}</div> + </div> + </div> +) + +const TeamSection = () => ( + <ContentSection name="team" title="Core Team"> + <div className="row justify-content-center"> + <TeamMember photoId="aiosup" name="Prof. dr. ir. Alexandru Iosup" description="Project Lead" /> + <TeamMember + photoId="gandreadis" + name="Georgios Andreadis" + description="Software Engineer responsible for the frontend web application" + /> + <TeamMember + photoId="fmastenbroek" + name="Fabian Mastenbroek" + description="Software Engineer responsible for the datacenter simulator" + /> + <TeamMember + photoId="jburley" + name="Jacob Burley" + description="Software Engineer responsible for prefabricated components" + /> + <TeamMember + photoId="loverweel" + name="Leon Overweel" + description="Former product lead and Software Engineer" + /> + </div> + <div className="text-center lead mt-3"> + See{' '} + <a target="_blank" href="http://atlarge.science/opendc#team" rel="noopener noreferrer"> + atlarge.science/opendc + </a>{' '} + for the full team! + </div> + </ContentSection> +) + +export default TeamSection diff --git a/opendc-web/opendc-web-ui/src/components/home/TechnologiesSection.js b/opendc-web/opendc-web-ui/src/components/home/TechnologiesSection.js new file mode 100644 index 00000000..c6013c71 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/home/TechnologiesSection.js @@ -0,0 +1,40 @@ +import React from 'react' +import FontAwesome from 'react-fontawesome' +import ContentSection from './ContentSection' + +const TechnologiesSection = () => ( + <ContentSection name="technologies" title="Technologies"> + <ul className="list-group text-left"> + <li className="d-flex list-group-item justify-content-between align-items-center list-group-item-primary"> + <span style={{ minWidth: 100 }}> + <FontAwesome name="window-maximize" className="mr-2" /> + <strong className="">Browser</strong> + </span> + <span className="text-right">JavaScript, React, Redux, Konva</span> + </li> + <li className="d-flex list-group-item justify-content-between align-items-center list-group-item-warning"> + <span style={{ minWidth: 100 }}> + <FontAwesome name="television" className="mr-2" /> + <strong>Server</strong> + </span> + <span className="text-right">Python, Flask, FlaskSocketIO, OpenAPI</span> + </li> + <li className="d-flex list-group-item justify-content-between align-items-center list-group-item-success"> + <span style={{ minWidth: 100 }}> + <FontAwesome name="database" className="mr-2" /> + <strong>Database</strong> + </span> + <span className="text-right">MongoDB</span> + </li> + <li className="d-flex list-group-item justify-content-between align-items-center list-group-item-danger"> + <span style={{ minWidth: 100 }}> + <FontAwesome name="cogs" className="mr-2" /> + <strong>Simulator</strong> + </span> + <span className="text-right">Kotlin</span> + </li> + </ul> + </ContentSection> +) + +export default TechnologiesSection diff --git a/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js b/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js new file mode 100644 index 00000000..589047dc --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/modals/ConfirmationModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Modal from './Modal' + +class ConfirmationModal extends React.Component { + static propTypes = { + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + show: PropTypes.bool.isRequired, + callback: PropTypes.func.isRequired, + } + + onConfirm() { + this.props.callback(true) + } + + onCancel() { + this.props.callback(false) + } + + render() { + return ( + <Modal + title={this.props.title} + show={this.props.show} + onSubmit={this.onConfirm.bind(this)} + onCancel={this.onCancel.bind(this)} + submitButtonType="danger" + submitButtonText="Confirm" + > + {this.props.message} + </Modal> + ) + } +} + +export default ConfirmationModal diff --git a/opendc-web/opendc-web-ui/src/components/modals/Modal.js b/opendc-web/opendc-web-ui/src/components/modals/Modal.js new file mode 100644 index 00000000..21b7f119 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/modals/Modal.js @@ -0,0 +1,53 @@ +import React, { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { Modal as RModal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap' + +function Modal({ children, title, show, onSubmit, onCancel, submitButtonType, submitButtonText }) { + const [modal, setModal] = useState(show) + + useEffect(() => setModal(show), [show]) + + const toggle = () => setModal(!modal) + const cancel = () => { + if (onCancel() !== false) { + toggle() + } + } + const submit = () => { + if (onSubmit() !== false) { + toggle() + } + } + + return ( + <RModal isOpen={modal} toggle={cancel}> + <ModalHeader toggle={cancel}>{title}</ModalHeader> + <ModalBody>{children}</ModalBody> + <ModalFooter> + <Button color="secondary" onClick={cancel}> + Close + </Button> + <Button color={submitButtonType} onClick={submit}> + {submitButtonText} + </Button> + </ModalFooter> + </RModal> + ) +} + +Modal.propTypes = { + title: PropTypes.string.isRequired, + show: PropTypes.bool.isRequired, + onSubmit: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + submitButtonType: PropTypes.string, + submitButtonText: PropTypes.string, +} + +Modal.defaultProps = { + submitButtonType: 'primary', + submitButtonText: 'Save', + show: false, +} + +export default Modal diff --git a/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js b/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js new file mode 100644 index 00000000..d0918c7e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/modals/TextInputModal.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Modal from './Modal' + +class TextInputModal extends React.Component { + static propTypes = { + title: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + show: PropTypes.bool.isRequired, + callback: PropTypes.func.isRequired, + initialValue: PropTypes.string, + } + + componentDidUpdate() { + if (this.props.initialValue && this.textInput) { + this.textInput.value = this.props.initialValue + } + } + + onSubmit() { + this.props.callback(this.textInput.value) + this.textInput.value = '' + } + + onCancel() { + this.props.callback(undefined) + this.textInput.value = '' + } + + render() { + return ( + <Modal + title={this.props.title} + show={this.props.show} + onSubmit={this.onSubmit.bind(this)} + onCancel={this.onCancel.bind(this)} + > + <form + onSubmit={(e) => { + e.preventDefault() + this.onSubmit() + }} + > + <div className="form-group"> + <label className="form-control-label">{this.props.label}</label> + <input type="text" className="form-control" ref={(textInput) => (this.textInput = textInput)} /> + </div> + </form> + </Modal> + ) + } +} + +export default TextInputModal diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js new file mode 100644 index 00000000..3c6b8724 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewPortfolioModalComponent.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types' +import React, { useRef } from 'react' +import { Form, FormGroup, Input, Label } from 'reactstrap' +import Modal from '../Modal' +import { AVAILABLE_METRICS, METRIC_NAMES } from '../../../util/available-metrics' + +const NewPortfolioModalComponent = ({ show, callback }) => { + const form = useRef(null) + const textInput = useRef(null) + const repeatsInput = useRef(null) + const metricCheckboxes = useRef({}) + + const onSubmit = () => { + if (form.current.reportValidity()) { + callback(textInput.current.value, { + enabledMetrics: AVAILABLE_METRICS.filter((metric) => metricCheckboxes.current[metric].checked), + repeatsPerScenario: parseInt(repeatsInput.current.value), + }) + + return true + } else { + return false + } + } + const onCancel = () => callback(undefined) + + return ( + <Modal title="New Portfolio" show={show} onSubmit={onSubmit} onCancel={onCancel}> + <Form + onSubmit={(e) => { + e.preventDefault() + this.onSubmit() + }} + innerRef={form} + > + <FormGroup> + <Label for="name">Name</Label> + <Input name="name" type="text" required innerRef={textInput} placeholder="My Portfolio" /> + </FormGroup> + <h4>Targets</h4> + <h5>Metrics</h5> + <FormGroup> + {AVAILABLE_METRICS.map((metric) => ( + <FormGroup check key={metric}> + <Label for={metric} check> + <Input + name={metric} + type="checkbox" + innerRef={(ref) => (metricCheckboxes.current[metric] = ref)} + /> + {METRIC_NAMES[metric]} + </Label> + </FormGroup> + ))} + </FormGroup> + <FormGroup> + <Label for="repeats">Repeats per scenario</Label> + <Input + name="repeats" + type="number" + required + innerRef={repeatsInput} + defaultValue="1" + min="1" + step="1" + /> + </FormGroup> + </Form> + </Modal> + ) +} + +NewPortfolioModalComponent.propTypes = { + show: PropTypes.bool.isRequired, + callback: PropTypes.func.isRequired, +} + +export default NewPortfolioModalComponent diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js new file mode 100644 index 00000000..01a5719c --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewScenarioModalComponent.js @@ -0,0 +1,144 @@ +import PropTypes from 'prop-types' +import React, { useRef } from 'react' +import { Form, FormGroup, Input, Label } from 'reactstrap' +import Shapes from '../../../shapes' +import Modal from '../Modal' + +const NewScenarioModalComponent = ({ + show, + callback, + currentPortfolioId, + currentPortfolioScenarioIds, + traces, + topologies, + schedulers, +}) => { + const form = useRef(null) + const textInput = useRef(null) + const traceSelect = useRef(null) + const traceLoadInput = useRef(null) + const topologySelect = useRef(null) + const failuresCheckbox = useRef(null) + const performanceInterferenceCheckbox = useRef(null) + const schedulerSelect = useRef(null) + + const onSubmit = () => { + if (!form.current.reportValidity()) { + return false + } + callback( + textInput.current.value, + currentPortfolioId, + { + traceId: traceSelect.current.value, + loadSamplingFraction: parseFloat(traceLoadInput.current.value), + }, + { + topologyId: topologySelect.current.value, + }, + { + failuresEnabled: failuresCheckbox.current.checked, + performanceInterferenceEnabled: performanceInterferenceCheckbox.current.checked, + schedulerName: schedulerSelect.current.value, + } + ) + return true + } + const onCancel = () => { + callback(undefined) + } + + return ( + <Modal title="New Scenario" show={show} onSubmit={onSubmit} onCancel={onCancel}> + <Form + onSubmit={(e) => { + e.preventDefault() + onSubmit() + }} + innerRef={form} + > + <FormGroup> + <Label for="name">Name</Label> + <Input + name="name" + type="text" + required + disabled={currentPortfolioScenarioIds.length === 0} + defaultValue={currentPortfolioScenarioIds.length === 0 ? 'Base scenario' : ''} + innerRef={textInput} + /> + </FormGroup> + <h4>Trace</h4> + <FormGroup> + <Label for="trace">Trace</Label> + <Input name="trace" type="select" innerRef={traceSelect}> + {traces.map((trace) => ( + <option value={trace._id} key={trace._id}> + {trace.name} + </option> + ))} + </Input> + </FormGroup> + <FormGroup> + <Label for="trace-load">Load sampling fraction</Label> + <Input + name="trace-load" + type="number" + innerRef={traceLoadInput} + required + defaultValue="1" + min="0" + max="1" + step="0.1" + /> + </FormGroup> + <h4>Topology</h4> + <div className="form-group"> + <Label for="topology">Topology</Label> + <Input name="topology" type="select" innerRef={topologySelect}> + {topologies.map((topology) => ( + <option value={topology._id} key={topology._id}> + {topology.name} + </option> + ))} + </Input> + </div> + <h4>Operational Phenomena</h4> + <FormGroup check> + <Label check for="failures"> + <Input type="checkbox" name="failures" innerRef={failuresCheckbox} />{' '} + <span className="ml-2">Enable failures</span> + </Label> + </FormGroup> + <FormGroup check> + <Label check for="perf-interference"> + <Input type="checkbox" name="perf-interference" innerRef={performanceInterferenceCheckbox} />{' '} + <span className="ml-2">Enable performance interference</span> + </Label> + </FormGroup> + <FormGroup> + <Label for="scheduler">Scheduler</Label> + <Input name="scheduler" type="select" innerRef={schedulerSelect}> + {schedulers.map((scheduler) => ( + <option value={scheduler.name} key={scheduler.name}> + {scheduler.name} + </option> + ))} + </Input> + </FormGroup> + </Form> + </Modal> + ) +} + +NewScenarioModalComponent.propTypes = { + show: PropTypes.bool.isRequired, + currentPortfolioId: PropTypes.string.isRequired, + currentPortfolioScenarioIds: PropTypes.arrayOf(PropTypes.string), + traces: PropTypes.arrayOf(Shapes.Trace), + topologies: PropTypes.arrayOf(Shapes.Topology), + schedulers: PropTypes.arrayOf(Shapes.Scheduler), + callback: PropTypes.func.isRequired, +} + +export default NewScenarioModalComponent diff --git a/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js new file mode 100644 index 00000000..9fee8831 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/modals/custom-components/NewTopologyModalComponent.js @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types' +import { Form, FormGroup, Input, Label } from 'reactstrap' +import React, { useRef } from 'react' +import Shapes from '../../../shapes' +import Modal from '../Modal' + +const NewTopologyModalComponent = ({ show, onCreateTopology, onDuplicateTopology, onCancel, topologies }) => { + const form = useRef(null) + const textInput = useRef(null) + const originTopology = useRef(null) + + const onCreate = () => { + onCreateTopology(textInput.current.value) + } + + const onDuplicate = () => { + onDuplicateTopology(textInput.current.value, originTopology.current.value) + } + + const onSubmit = () => { + if (!form.current.reportValidity()) { + return false + } else if (originTopology.current.selectedIndex === 0) { + onCreate() + } else { + onDuplicate() + } + + return true + } + + return ( + <Modal title="New Topology" show={show} onSubmit={onSubmit} onCancel={onCancel}> + <Form + onSubmit={(e) => { + e.preventDefault() + onSubmit() + }} + innerRef={form} + > + <FormGroup> + <Label for="name">Name</Label> + <Input name="name" type="text" required innerRef={textInput} /> + </FormGroup> + <FormGroup> + <Label for="origin">Topology to duplicate</Label> + <Input name="origin" type="select" innerRef={originTopology}> + <option value={-1} key={-1}> + None - start from scratch + </option> + {topologies.map((topology) => ( + <option value={topology._id} key={topology._id}> + {topology.name} + </option> + ))} + </Input> + </FormGroup> + </Form> + </Modal> + ) +} + +NewTopologyModalComponent.propTypes = { + show: PropTypes.bool.isRequired, + topologies: PropTypes.arrayOf(Shapes.Topology), + onCreateTopology: PropTypes.func.isRequired, + onDuplicateTopology: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +} + +export default NewTopologyModalComponent diff --git a/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js b/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js new file mode 100644 index 00000000..c5de3d0b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/navigation/AppNavbarComponent.js @@ -0,0 +1,26 @@ +import React from 'react' +import FontAwesome from 'react-fontawesome' +import { Link } from 'react-router-dom' +import { NavLink } from 'reactstrap' +import Navbar, { NavItem } from './Navbar' +import './Navbar.sass' + +const AppNavbarComponent = ({ project, fullWidth }) => ( + <Navbar fullWidth={fullWidth}> + <NavItem route="/projects"> + <NavLink tag={Link} title="My Projects" to="/projects"> + <FontAwesome name="list" className="mr-2" /> + My Projects + </NavLink> + </NavItem> + {project ? ( + <NavItem> + <NavLink tag={Link} title="Current Project" to={`/projects/${project._id}`}> + <span>{project.name}</span> + </NavLink> + </NavItem> + ) : undefined} + </Navbar> +) + +export default AppNavbarComponent diff --git a/opendc-web/opendc-web-ui/src/components/navigation/HomeNavbar.js b/opendc-web/opendc-web-ui/src/components/navigation/HomeNavbar.js new file mode 100644 index 00000000..08d222ea --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/navigation/HomeNavbar.js @@ -0,0 +1,23 @@ +import React from 'react' +import { NavItem, NavLink } from 'reactstrap' +import Navbar from './Navbar' +import './Navbar.sass' + +const ScrollNavItem = ({ id, name }) => ( + <NavItem> + <NavLink href={id}>{name}</NavLink> + </NavItem> +) + +const HomeNavbar = () => ( + <Navbar fullWidth={false}> + <ScrollNavItem id="#stakeholders" name="Stakeholders" /> + <ScrollNavItem id="#modeling" name="Modeling" /> + <ScrollNavItem id="#project" name="Project" /> + <ScrollNavItem id="#technologies" name="Technologies" /> + <ScrollNavItem id="#team" name="Team" /> + <ScrollNavItem id="#contact" name="Contact" /> + </Navbar> +) + +export default HomeNavbar diff --git a/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js b/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js new file mode 100644 index 00000000..78b02b44 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/navigation/LogoutButton.js @@ -0,0 +1,17 @@ +import PropTypes from 'prop-types' +import React from 'react' +import FontAwesome from 'react-fontawesome' +import { Link } from 'react-router-dom' +import { NavLink } from 'reactstrap' + +const LogoutButton = ({ onLogout }) => ( + <NavLink tag={Link} className="logout" title="Sign out" to="#" onClick={onLogout}> + <FontAwesome name="power-off" size="lg" /> + </NavLink> +) + +LogoutButton.propTypes = { + onLogout: PropTypes.func.isRequired, +} + +export default LogoutButton diff --git a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js new file mode 100644 index 00000000..55f98900 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js @@ -0,0 +1,92 @@ +import React, { useState } from 'react' +import { Link, useLocation } from 'react-router-dom' +import { + Navbar as RNavbar, + NavItem as RNavItem, + NavLink, + NavbarBrand, + NavbarToggler, + Collapse, + Nav, + Container, +} from 'reactstrap' +import { userIsLoggedIn } from '../../auth/index' +import Login from '../../containers/auth/Login' +import Logout from '../../containers/auth/Logout' +import ProfileName from '../../containers/auth/ProfileName' +import './Navbar.sass' + +export const NAVBAR_HEIGHT = 60 + +const GitHubLink = () => ( + <a + href="https://github.com/atlarge-research/opendc" + className="ml-2 mr-3 text-dark" + style={{ position: 'relative', top: 7 }} + > + <span className="fa fa-github fa-2x" /> + </a> +) + +export const NavItem = ({ route, children }) => { + const location = useLocation() + return <RNavItem active={location.pathname === route}>{children}</RNavItem> +} + +export const LoggedInSection = () => { + const location = useLocation() + return ( + <Nav navbar className="auth-links"> + {userIsLoggedIn() ? ( + [ + location.pathname === '/' ? ( + <NavItem route="/projects" key="projects"> + <NavLink tag={Link} title="My Projects" to="/projects"> + My Projects + </NavLink> + </NavItem> + ) : ( + <NavItem route="/profile" key="profile"> + <NavLink tag={Link} title="My Profile" to="/profile"> + <ProfileName /> + </NavLink> + </NavItem> + ), + <NavItem route="logout" key="logout"> + <Logout /> + </NavItem>, + ] + ) : ( + <NavItem route="login"> + <GitHubLink /> + <Login visible={true} /> + </NavItem> + )} + </Nav> + ) +} + +const Navbar = ({ fullWidth, children }) => { + const [isOpen, setIsOpen] = useState(false) + const toggle = () => setIsOpen(!isOpen) + + return ( + <RNavbar fixed="top" color="light" light expand="lg" id="navbar"> + <Container fluid={fullWidth}> + <NavbarToggler onClick={toggle} /> + <NavbarBrand tag={Link} to="/" title="OpenDC" className="opendc-brand"> + <img src="/img/logo.png" alt="OpenDC" /> + </NavbarBrand> + + <Collapse isOpen={isOpen} navbar> + <Nav className="mr-auto" navbar> + {children} + </Nav> + <LoggedInSection /> + </Collapse> + </Container> + </RNavbar> + ) +} + +export default Navbar diff --git a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.sass b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.sass new file mode 100644 index 00000000..c9d2aad2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.sass @@ -0,0 +1,30 @@ +@import ../../style-globals/_mixins.sass +@import ../../style-globals/_variables.sass + +.navbar + border-top: $blue 3px solid + border-bottom: $gray-semi-dark 1px solid + color: $gray-very-dark + background: #fafafb + +.opendc-brand + display: inline-block + color: $gray-very-dark + + +transition(background, $transition-length) + + img + position: relative + bottom: 3px + display: inline-block + width: 30px + +.login + height: 40px + background: $blue + border: none + padding-top: 10px + +clickable + + &:hover + background: $blue-dark diff --git a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js b/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js new file mode 100644 index 00000000..dbdba212 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.js @@ -0,0 +1,6 @@ +import React from 'react' +import './BlinkingCursor.sass' + +const BlinkingCursor = () => <span className="blinking-cursor">_</span> + +export default BlinkingCursor diff --git a/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.sass b/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.sass new file mode 100644 index 00000000..ad91df85 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/not-found/BlinkingCursor.sass @@ -0,0 +1,35 @@ +.blinking-cursor + -webkit-animation: 1s blink step-end infinite + -moz-animation: 1s blink step-end infinite + -o-animation: 1s blink step-end infinite + animation: 1s blink step-end infinite + +@keyframes blink + from, to + color: #eeeeee + 50% + color: #333333 + +@-moz-keyframes blink + from, to + color: #eeeeee + 50% + color: #333333 + +@-webkit-keyframes blink + from, to + color: #eeeeee + 50% + color: #333333 + +@-ms-keyframes blink + from, to + color: #eeeeee + 50% + color: #333333 + +@-o-keyframes blink + from, to + color: #eeeeee + 50% + color: #333333 diff --git a/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js new file mode 100644 index 00000000..bcc522c9 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.js @@ -0,0 +1,28 @@ +import React from 'react' +import './CodeBlock.sass' + +const CodeBlock = () => { + const textBlock = + ' oo oooo oo <br/>' + + ' oo oo oo oo <br/>' + + ' oo oo oo oo <br/>' + + ' oooooo oo oo oooooo <br/>' + + ' oo oo oo oo <br/>' + + ' oo oooo oo <br/>' + const charList = textBlock.split('') + + // Binary representation of the string "OpenDC!" ;) + const binaryString = '01001111011100000110010101101110010001000100001100100001' + + let binaryIndex = 0 + for (let i = 0; i < charList.length; i++) { + if (charList[i] === 'o') { + charList[i] = binaryString[binaryIndex] + binaryIndex++ + } + } + + return <div className="code-block" dangerouslySetInnerHTML={{ __html: textBlock }} /> +} + +export default CodeBlock diff --git a/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.sass b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.sass new file mode 100644 index 00000000..e452f917 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/not-found/CodeBlock.sass @@ -0,0 +1,3 @@ +.code-block + white-space: pre-wrap + margin-top: 60px diff --git a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js new file mode 100644 index 00000000..a25e558a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.js @@ -0,0 +1,33 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import BlinkingCursor from './BlinkingCursor' +import CodeBlock from './CodeBlock' +import './TerminalWindow.sass' + +const TerminalWindow = () => ( + <div className="terminal-window"> + <div className="terminal-header">Terminal -- bash</div> + <div className="terminal-body"> + <div className="segfault"> + $ status + <br /> + opendc[4264]: segfault at 0000051497be459d1 err 12 in libopendc.9.0.4 + <br /> + opendc[4269]: segfault at 000004234855fc2db err 3 in libopendc.9.0.4 + <br /> + opendc[4270]: STDERR Page does not exist + <br /> + </div> + <CodeBlock /> + <div className="sub-title"> + Got lost? + <BlinkingCursor /> + </div> + <Link to="/" className="home-btn"> + <span className="fa fa-home" /> GET ME BACK TO OPENDC + </Link> + </div> + </div> +) + +export default TerminalWindow diff --git a/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.sass b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.sass new file mode 100644 index 00000000..7f05335a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/not-found/TerminalWindow.sass @@ -0,0 +1,70 @@ +.terminal-window + width: 600px + height: 400px + display: block + + position: absolute + top: 0 + bottom: 0 + left: 0 + right: 0 + + margin: auto + + -webkit-user-select: none + -moz-user-select: none + -ms-user-select: none + user-select: none + cursor: default + + overflow: hidden + + box-shadow: 5px 5px 20px #444444 + +.terminal-header + font-family: monospace + background: #cccccc + color: #444444 + height: 30px + line-height: 30px + padding-left: 10px + + border-top-left-radius: 7px + border-top-right-radius: 7px + +.terminal-body + font-family: monospace + text-align: center + background-color: #333333 + color: #eeeeee + padding: 10px + + height: 100% + +.segfault + text-align: left + +.sub-title + margin-top: 20px + +.home-btn + margin-top: 10px + padding: 5px + display: inline-block + border: 1px solid #eeeeee + color: #eeeeee + text-decoration: none + cursor: pointer + + -webkit-transition: all 200ms + -moz-transition: all 200ms + -o-transition: all 200ms + transition: all 200ms + +.home-btn:hover + background: #eeeeee + color: #333333 + +.home-btn:active + background: #333333 + color: #eeeeee diff --git a/opendc-web/opendc-web-ui/src/components/projects/FilterButton.js b/opendc-web/opendc-web-ui/src/components/projects/FilterButton.js new file mode 100644 index 00000000..664f9b46 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/FilterButton.js @@ -0,0 +1,24 @@ +import classNames from 'classnames' +import PropTypes from 'prop-types' +import React from 'react' + +const FilterButton = ({ active, children, onClick }) => ( + <button + className={classNames('btn btn-secondary', { active: active })} + onClick={() => { + if (!active) { + onClick() + } + }} + > + {children} + </button> +) + +FilterButton.propTypes = { + active: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, + onClick: PropTypes.func.isRequired, +} + +export default FilterButton diff --git a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js new file mode 100644 index 00000000..2b9795d0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.js @@ -0,0 +1,13 @@ +import React from 'react' +import FilterLink from '../../containers/projects/FilterLink' +import './FilterPanel.sass' + +const FilterPanel = () => ( + <div className="btn-group filter-panel mb-2"> + <FilterLink filter="SHOW_ALL">All Projects</FilterLink> + <FilterLink filter="SHOW_OWN">My Projects</FilterLink> + <FilterLink filter="SHOW_SHARED">Shared with me</FilterLink> + </div> +) + +export default FilterPanel diff --git a/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.sass b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.sass new file mode 100644 index 00000000..f71cf6c8 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/FilterPanel.sass @@ -0,0 +1,5 @@ +.filter-panel + display: flex + + button + flex: 1 !important diff --git a/opendc-web/opendc-web-ui/src/components/projects/NewProjectButtonComponent.js b/opendc-web/opendc-web-ui/src/components/projects/NewProjectButtonComponent.js new file mode 100644 index 00000000..312671c6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/NewProjectButtonComponent.js @@ -0,0 +1,17 @@ +import PropTypes from 'prop-types' +import React from 'react' + +const NewProjectButtonComponent = ({ onClick }) => ( + <div className="bottom-btn-container"> + <div className="btn btn-primary float-right" onClick={onClick}> + <span className="fa fa-plus mr-2" /> + New Project + </div> + </div> +) + +NewProjectButtonComponent.propTypes = { + onClick: PropTypes.func.isRequired, +} + +export default NewProjectButtonComponent diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js new file mode 100644 index 00000000..1c76cc7f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectActionButtons.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Link } from 'react-router-dom' + +const ProjectActionButtons = ({ projectId, onViewUsers, onDelete }) => ( + <td className="text-right"> + <Link to={'/projects/' + projectId} className="btn btn-outline-primary btn-sm mr-2" title="Open this project"> + <span className="fa fa-play" /> + </Link> + <div + className="btn btn-outline-success btn-sm disabled mr-2" + title="View and edit collaborators (not supported currently)" + onClick={() => onViewUsers(projectId)} + > + <span className="fa fa-users" /> + </div> + <div className="btn btn-outline-danger btn-sm" title="Delete this project" onClick={() => onDelete(projectId)}> + <span className="fa fa-trash" /> + </div> + </td> +) + +ProjectActionButtons.propTypes = { + projectId: PropTypes.string.isRequired, + onViewUsers: PropTypes.func, + onDelete: PropTypes.func, +} + +export default ProjectActionButtons diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthList.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthList.js new file mode 100644 index 00000000..8eb4f93b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthList.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types' +import React from 'react' +import Shapes from '../../shapes/index' +import ProjectAuthRow from './ProjectAuthRow' + +const ProjectAuthList = ({ authorizations }) => { + return ( + <div className="vertically-expanding-container"> + {authorizations.length === 0 ? ( + <div className="alert alert-info"> + <span className="info-icon fa fa-question-circle mr-2" /> + <strong>No projects here yet...</strong> Add some with the 'New Project' button! + </div> + ) : ( + <table className="table table-striped"> + <thead> + <tr> + <th>Project name</th> + <th>Last edited</th> + <th>Access rights</th> + <th /> + </tr> + </thead> + <tbody> + {authorizations.map((authorization) => ( + <ProjectAuthRow projectAuth={authorization} key={authorization.project._id} /> + ))} + </tbody> + </table> + )} + </div> + ) +} + +ProjectAuthList.propTypes = { + authorizations: PropTypes.arrayOf(Shapes.Authorization).isRequired, +} + +export default ProjectAuthList diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthRow.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthRow.js new file mode 100644 index 00000000..3f904061 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectAuthRow.js @@ -0,0 +1,24 @@ +import classNames from 'classnames' +import React from 'react' +import ProjectActions from '../../containers/projects/ProjectActions' +import Shapes from '../../shapes/index' +import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from '../../util/authorizations' +import { parseAndFormatDateTime } from '../../util/date-time' + +const ProjectAuthRow = ({ projectAuth }) => ( + <tr> + <td className="pt-3">{projectAuth.project.name}</td> + <td className="pt-3">{parseAndFormatDateTime(projectAuth.project.datetimeLastEdited)}</td> + <td className="pt-3"> + <span className={classNames('fa', 'fa-' + AUTH_ICON_MAP[projectAuth.authorizationLevel], 'mr-2')} /> + {AUTH_DESCRIPTION_MAP[projectAuth.authorizationLevel]} + </td> + <ProjectActions projectId={projectAuth.project._id} /> + </tr> +) + +ProjectAuthRow.propTypes = { + projectAuth: Shapes.Authorization.isRequired, +} + +export default ProjectAuthRow diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js new file mode 100644 index 00000000..9e4a6969 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/GrayContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { goDownOneInteractionLevel } from '../../../actions/interaction-level' +import GrayLayer from '../../../components/app/map/elements/GrayLayer' + +const mapDispatchToProps = (dispatch) => { + return { + onClick: () => dispatch(goDownOneInteractionLevel()), + } +} + +const GrayContainer = connect(undefined, mapDispatchToProps)(GrayLayer) + +export default GrayContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js b/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js new file mode 100644 index 00000000..23c920b6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/MapStage.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux' +import { setMapDimensions, setMapPositionWithBoundsCheck, zoomInOnPosition } from '../../../actions/map' +import MapStageComponent from '../../../components/app/map/MapStageComponent' + +const mapStateToProps = (state) => { + return { + mapPosition: state.map.position, + mapDimensions: state.map.dimensions, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + zoomInOnPosition: (zoomIn, x, y) => dispatch(zoomInOnPosition(zoomIn, x, y)), + setMapPositionWithBoundsCheck: (x, y) => dispatch(setMapPositionWithBoundsCheck(x, y)), + setMapDimensions: (width, height) => dispatch(setMapDimensions(width, height)), + } +} + +const MapStage = connect(mapStateToProps, mapDispatchToProps)(MapStageComponent) + +export default MapStage diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js new file mode 100644 index 00000000..40077608 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/RackContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import RackGroup from '../../../components/app/map/groups/RackGroup' + +const mapStateToProps = (state) => { + return { + interactionLevel: state.interactionLevel, + } +} + +const RackContainer = connect(mapStateToProps)(RackGroup) + +export default RackContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js new file mode 100644 index 00000000..53746271 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/RackEnergyFillContainer.js @@ -0,0 +1,26 @@ +import { connect } from 'react-redux' +import RackFillBar from '../../../components/app/map/elements/RackFillBar' + +const mapStateToProps = (state, ownProps) => { + let energyConsumptionTotal = 0 + const rack = state.objects.rack[state.objects.tile[ownProps.tileId].rackId] + const machineIds = rack.machineIds + machineIds.forEach((machineId) => { + if (machineId !== null) { + const machine = state.objects.machine[machineId] + machine.cpuIds.forEach((id) => (energyConsumptionTotal += state.objects.cpu[id].energyConsumptionW)) + machine.gpuIds.forEach((id) => (energyConsumptionTotal += state.objects.gpu[id].energyConsumptionW)) + machine.memoryIds.forEach((id) => (energyConsumptionTotal += state.objects.memory[id].energyConsumptionW)) + machine.storageIds.forEach((id) => (energyConsumptionTotal += state.objects.storage[id].energyConsumptionW)) + } + }) + + return { + type: 'energy', + fillFraction: Math.min(1, energyConsumptionTotal / rack.powerCapacityW), + } +} + +const RackSpaceFillContainer = connect(mapStateToProps)(RackFillBar) + +export default RackSpaceFillContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js new file mode 100644 index 00000000..0509a5a5 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/RackSpaceFillContainer.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux' +import RackFillBar from '../../../components/app/map/elements/RackFillBar' + +const mapStateToProps = (state, ownProps) => { + const machineIds = state.objects.rack[state.objects.tile[ownProps.tileId].rackId].machineIds + return { + type: 'space', + fillFraction: machineIds.filter((id) => id !== null).length / machineIds.length, + } +} + +const RackSpaceFillContainer = connect(mapStateToProps)(RackFillBar) + +export default RackSpaceFillContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js new file mode 100644 index 00000000..91bf4e5d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/RoomContainer.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import { goFromBuildingToRoom } from '../../../actions/interaction-level' +import RoomGroup from '../../../components/app/map/groups/RoomGroup' + +const mapStateToProps = (state, ownProps) => { + return { + interactionLevel: state.interactionLevel, + currentRoomInConstruction: state.construction.currentRoomInConstruction, + room: state.objects.room[ownProps.roomId], + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onClick: () => dispatch(goFromBuildingToRoom(ownProps.roomId)), + } +} + +const RoomContainer = connect(mapStateToProps, mapDispatchToProps)(RoomGroup) + +export default RoomContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js new file mode 100644 index 00000000..04d6c8d6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/TileContainer.js @@ -0,0 +1,26 @@ +import { connect } from 'react-redux' +import { goFromRoomToRack } from '../../../actions/interaction-level' +import TileGroup from '../../../components/app/map/groups/TileGroup' + +const mapStateToProps = (state, ownProps) => { + const tile = state.objects.tile[ownProps.tileId] + + return { + interactionLevel: state.interactionLevel, + tile, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onClick: (tile) => { + if (tile.rackId) { + dispatch(goFromRoomToRack(tile._id)) + } + }, + } +} + +const TileContainer = connect(mapStateToProps, mapDispatchToProps)(TileGroup) + +export default TileContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js new file mode 100644 index 00000000..de43a151 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/TopologyContainer.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux' +import TopologyGroup from '../../../components/app/map/groups/TopologyGroup' + +const mapStateToProps = (state) => { + if (state.currentTopologyId === '-1') { + return {} + } + + return { + topology: state.objects.topology[state.currentTopologyId], + interactionLevel: state.interactionLevel, + } +} + +const TopologyContainer = connect(mapStateToProps)(TopologyGroup) + +export default TopologyContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js new file mode 100644 index 00000000..67f8a242 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/WallContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import WallGroup from '../../../components/app/map/groups/WallGroup' + +const mapStateToProps = (state, ownProps) => { + return { + tiles: state.objects.room[ownProps.roomId].tileIds.map((tileId) => state.objects.tile[tileId]), + } +} + +const WallContainer = connect(mapStateToProps)(WallGroup) + +export default WallContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js new file mode 100644 index 00000000..fa3b9d22 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/controls/ScaleIndicatorContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import ScaleIndicatorComponent from '../../../../components/app/map/controls/ScaleIndicatorComponent' + +const mapStateToProps = (state) => { + return { + scale: state.map.scale, + } +} + +const ScaleIndicatorContainer = connect(mapStateToProps)(ScaleIndicatorComponent) + +export default ScaleIndicatorContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js b/opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js new file mode 100644 index 00000000..ddc68cc7 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/controls/ZoomControlContainer.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux' +import { zoomInOnCenter } from '../../../../actions/map' +import ZoomControlComponent from '../../../../components/app/map/controls/ZoomControlComponent' + +const mapStateToProps = (state) => { + return { + mapScale: state.map.scale, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + zoomInOnCenter: (zoomIn) => dispatch(zoomInOnCenter(zoomIn)), + } +} + +const ZoomControlContainer = connect(mapStateToProps, mapDispatchToProps)(ZoomControlComponent) + +export default ZoomControlContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js b/opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js new file mode 100644 index 00000000..8596cb9c --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/layers/MapLayer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import MapLayerComponent from '../../../../components/app/map/layers/MapLayerComponent' + +const mapStateToProps = (state) => { + return { + mapPosition: state.map.position, + mapScale: state.map.scale, + } +} + +const MapLayer = connect(mapStateToProps)(MapLayerComponent) + +export default MapLayer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js b/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js new file mode 100644 index 00000000..a4927862 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/layers/ObjectHoverLayer.js @@ -0,0 +1,33 @@ +import { connect } from 'react-redux' +import { addRackToTile } from '../../../../actions/topology/room' +import ObjectHoverLayerComponent from '../../../../components/app/map/layers/ObjectHoverLayerComponent' +import { findTileWithPosition } from '../../../../util/tile-calculations' + +const mapStateToProps = (state) => { + return { + mapPosition: state.map.position, + mapScale: state.map.scale, + isEnabled: () => state.construction.inRackConstructionMode, + isValid: (x, y) => { + if (state.interactionLevel.mode !== 'ROOM') { + return false + } + + const currentRoom = state.objects.room[state.interactionLevel.roomId] + const tiles = currentRoom.tileIds.map((tileId) => state.objects.tile[tileId]) + const tile = findTileWithPosition(tiles, x, y) + + return !(tile === null || tile.rackId) + }, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onClick: (x, y) => dispatch(addRackToTile(x, y)), + } +} + +const ObjectHoverLayer = connect(mapStateToProps, mapDispatchToProps)(ObjectHoverLayerComponent) + +export default ObjectHoverLayer diff --git a/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js new file mode 100644 index 00000000..66404f9e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/map/layers/RoomHoverLayer.js @@ -0,0 +1,46 @@ +import { connect } from 'react-redux' +import { toggleTileAtLocation } from '../../../../actions/topology/building' +import RoomHoverLayerComponent from '../../../../components/app/map/layers/RoomHoverLayerComponent' +import { + deriveValidNextTilePositions, + findPositionInPositions, + findPositionInRooms, +} from '../../../../util/tile-calculations' + +const mapStateToProps = (state) => { + return { + mapPosition: state.map.position, + mapScale: state.map.scale, + isEnabled: () => state.construction.currentRoomInConstruction !== '-1', + isValid: (x, y) => { + const newRoom = Object.assign({}, state.objects.room[state.construction.currentRoomInConstruction]) + const oldRooms = Object.keys(state.objects.room) + .map((id) => Object.assign({}, state.objects.room[id])) + .filter( + (room) => + state.objects.topology[state.currentTopologyId].roomIds.indexOf(room._id) !== -1 && + room._id !== state.construction.currentRoomInConstruction + ) + + ;[...oldRooms, newRoom].forEach((room) => { + room.tiles = room.tileIds.map((tileId) => state.objects.tile[tileId]) + }) + if (newRoom.tileIds.length === 0) { + return findPositionInRooms(oldRooms, x, y) === -1 + } + + const validNextPositions = deriveValidNextTilePositions(oldRooms, newRoom.tiles) + return findPositionInPositions(validNextPositions, x, y) !== -1 + }, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onClick: (x, y) => dispatch(toggleTileAtLocation(x, y)), + } +} + +const RoomHoverLayer = connect(mapStateToProps, mapDispatchToProps)(RoomHoverLayerComponent) + +export default RoomHoverLayer diff --git a/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js b/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js new file mode 100644 index 00000000..4b430e54 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/results/PortfolioResultsContainer.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux' +import PortfolioResultsComponent from '../../../components/app/results/PortfolioResultsComponent' + +const mapStateToProps = (state) => { + if ( + state.currentPortfolioId === '-1' || + !state.objects.portfolio[state.currentPortfolioId] || + state.objects.portfolio[state.currentPortfolioId].scenarioIds + .map((scenarioId) => state.objects.scenario[scenarioId]) + .some((s) => s === undefined) + ) { + return { + portfolio: undefined, + scenarios: [], + } + } + + return { + portfolio: state.objects.portfolio[state.currentPortfolioId], + scenarios: state.objects.portfolio[state.currentPortfolioId].scenarioIds.map( + (scenarioId) => state.objects.scenario[scenarioId] + ), + } +} + +const PortfolioResultsContainer = connect(mapStateToProps)(PortfolioResultsComponent) + +export default PortfolioResultsContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js new file mode 100644 index 00000000..b32c8b1d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/PortfolioListContainer.js @@ -0,0 +1,45 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import PortfolioListComponent from '../../../../components/app/sidebars/project/PortfolioListComponent' +import { deletePortfolio, setCurrentPortfolio } from '../../../../actions/portfolios' +import { openNewPortfolioModal } from '../../../../actions/modals/portfolios' +import { getState } from '../../../../util/state-utils' +import { setCurrentTopology } from '../../../../actions/topology/building' + +const mapStateToProps = (state) => { + let portfolios = state.objects.project[state.currentProjectId] + ? state.objects.project[state.currentProjectId].portfolioIds.map((t) => state.objects.portfolio[t]) + : [] + if (portfolios.filter((t) => !t).length > 0) { + portfolios = [] + } + + return { + currentProjectId: state.currentProjectId, + currentPortfolioId: state.currentPortfolioId, + portfolios, + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onNewPortfolio: () => { + dispatch(openNewPortfolioModal()) + }, + onChoosePortfolio: (portfolioId) => { + dispatch(setCurrentPortfolio(portfolioId)) + }, + onDeletePortfolio: async (id) => { + if (id) { + const state = await getState(dispatch) + dispatch(deletePortfolio(id)) + dispatch(setCurrentTopology(state.objects.project[state.currentProjectId].topologyIds[0])) + ownProps.history.push(`/projects/${state.currentProjectId}`) + } + }, + } +} + +const PortfolioListContainer = withRouter(connect(mapStateToProps, mapDispatchToProps)(PortfolioListComponent)) + +export default PortfolioListContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js new file mode 100644 index 00000000..49001099 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ProjectSidebarContainer.js @@ -0,0 +1,10 @@ +import React from 'react' +import { withRouter } from 'react-router-dom' +import ProjectSidebarComponent from '../../../../components/app/sidebars/project/ProjectSidebarComponent' +import { isCollapsible } from '../../../../util/sidebar-space' + +const ProjectSidebarContainer = withRouter(({ location, ...props }) => ( + <ProjectSidebarComponent collapsible={isCollapsible(location)} {...props} /> +)) + +export default ProjectSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js new file mode 100644 index 00000000..415e2792 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js @@ -0,0 +1,41 @@ +import { connect } from 'react-redux' +import ScenarioListComponent from '../../../../components/app/sidebars/project/ScenarioListComponent' +import { openNewScenarioModal } from '../../../../actions/modals/scenarios' +import { deleteScenario, setCurrentScenario } from '../../../../actions/scenarios' +import { setCurrentPortfolio } from '../../../../actions/portfolios' + +const mapStateToProps = (state, ownProps) => { + let scenarios = state.objects.portfolio[ownProps.portfolioId] + ? state.objects.portfolio[ownProps.portfolioId].scenarioIds.map((t) => state.objects.scenario[t]) + : [] + if (scenarios.filter((t) => !t).length > 0) { + scenarios = [] + } + + return { + currentProjectId: state.currentProjectId, + currentScenarioId: state.currentScenarioId, + scenarios, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onNewScenario: (currentPortfolioId) => { + dispatch(setCurrentPortfolio(currentPortfolioId)) + dispatch(openNewScenarioModal()) + }, + onChooseScenario: (portfolioId, scenarioId) => { + dispatch(setCurrentScenario(portfolioId, scenarioId)) + }, + onDeleteScenario: (id) => { + if (id) { + dispatch(deleteScenario(id)) + } + }, + } +} + +const ScenarioListContainer = connect(mapStateToProps, mapDispatchToProps)(ScenarioListComponent) + +export default ScenarioListContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js new file mode 100644 index 00000000..e1de18f9 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/TopologyListContainer.js @@ -0,0 +1,46 @@ +import { connect } from 'react-redux' +import TopologyListComponent from '../../../../components/app/sidebars/project/TopologyListComponent' +import { setCurrentTopology } from '../../../../actions/topology/building' +import { openNewTopologyModal } from '../../../../actions/modals/topology' +import { withRouter } from 'react-router-dom' +import { getState } from '../../../../util/state-utils' +import { deleteTopology } from '../../../../actions/topologies' + +const mapStateToProps = (state) => { + let topologies = state.objects.project[state.currentProjectId] + ? state.objects.project[state.currentProjectId].topologyIds.map((t) => state.objects.topology[t]) + : [] + if (topologies.filter((t) => !t).length > 0) { + topologies = [] + } + + return { + currentTopologyId: state.currentTopologyId, + topologies, + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onChooseTopology: async (id) => { + dispatch(setCurrentTopology(id)) + const state = await getState(dispatch) + ownProps.history.push(`/projects/${state.currentProjectId}`) + }, + onNewTopology: () => { + dispatch(openNewTopologyModal()) + }, + onDeleteTopology: async (id) => { + if (id) { + const state = await getState(dispatch) + dispatch(deleteTopology(id)) + dispatch(setCurrentTopology(state.objects.project[state.currentProjectId].topologyIds[0])) + ownProps.history.push(`/projects/${state.currentProjectId}`) + } + }, + } +} + +const TopologyListContainer = withRouter(connect(mapStateToProps, mapDispatchToProps)(TopologyListComponent)) + +export default TopologyListContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js new file mode 100644 index 00000000..fe7c02fd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/TopologySidebarContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import TopologySidebarComponent from '../../../../components/app/sidebars/topology/TopologySidebarComponent' + +const mapStateToProps = (state) => { + return { + interactionLevel: state.interactionLevel, + } +} + +const TopologySidebarContainer = connect(mapStateToProps)(TopologySidebarComponent) + +export default TopologySidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js new file mode 100644 index 00000000..a0b52e56 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/BuildingSidebarContainer.js @@ -0,0 +1,5 @@ +import BuildingSidebarComponent from '../../../../../components/app/sidebars/topology/building/BuildingSidebarComponent' + +const BuildingSidebarContainer = BuildingSidebarComponent + +export default BuildingSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js new file mode 100644 index 00000000..ea9e9e60 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/building/NewRoomConstructionContainer.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux' +import { + cancelNewRoomConstruction, + finishNewRoomConstruction, + startNewRoomConstruction, +} from '../../../../../actions/topology/building' +import StartNewRoomConstructionComponent from '../../../../../components/app/sidebars/topology/building/NewRoomConstructionComponent' + +const mapStateToProps = (state) => { + return { + currentRoomInConstruction: state.construction.currentRoomInConstruction, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onStart: () => dispatch(startNewRoomConstruction()), + onFinish: () => dispatch(finishNewRoomConstruction()), + onCancel: () => dispatch(cancelNewRoomConstruction()), + } +} + +const NewRoomConstructionButton = connect(mapStateToProps, mapDispatchToProps)(StartNewRoomConstructionComponent) + +export default NewRoomConstructionButton diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js new file mode 100644 index 00000000..24287ab0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/BackToRackContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { goDownOneInteractionLevel } from '../../../../../actions/interaction-level' +import BackToRackComponent from '../../../../../components/app/sidebars/topology/machine/BackToRackComponent' + +const mapDispatchToProps = (dispatch) => { + return { + onClick: () => dispatch(goDownOneInteractionLevel()), + } +} + +const BackToRackContainer = connect(undefined, mapDispatchToProps)(BackToRackComponent) + +export default BackToRackContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js new file mode 100644 index 00000000..65e683e6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/DeleteMachineContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { openDeleteMachineModal } from '../../../../../actions/modals/topology' +import DeleteMachineComponent from '../../../../../components/app/sidebars/topology/machine/DeleteMachineComponent' + +const mapDispatchToProps = (dispatch) => { + return { + onClick: () => dispatch(openDeleteMachineModal()), + } +} + +const DeleteMachineContainer = connect(undefined, mapDispatchToProps)(DeleteMachineComponent) + +export default DeleteMachineContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js new file mode 100644 index 00000000..1cf35b05 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineNameContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import MachineNameComponent from '../../../../../components/app/sidebars/topology/machine/MachineNameComponent' + +const mapStateToProps = (state) => { + return { + position: state.interactionLevel.position, + } +} + +const MachineNameContainer = connect(mapStateToProps)(MachineNameComponent) + +export default MachineNameContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js new file mode 100644 index 00000000..b04e3118 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/MachineSidebarContainer.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux' +import MachineSidebarComponent from '../../../../../components/app/sidebars/topology/machine/MachineSidebarComponent' + +const mapStateToProps = (state) => { + return { + machineId: + state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].machineIds[ + state.interactionLevel.position - 1 + ], + } +} + +const MachineSidebarContainer = connect(mapStateToProps)(MachineSidebarComponent) + +export default MachineSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js new file mode 100644 index 00000000..29e48016 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitAddContainer.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux' +import { addUnit } from '../../../../../actions/topology/machine' +import UnitAddComponent from '../../../../../components/app/sidebars/topology/machine/UnitAddComponent' + +const mapStateToProps = (state, ownProps) => { + return { + units: Object.values(state.objects[ownProps.unitType]), + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onAdd: (id) => dispatch(addUnit(ownProps.unitType, id)), + } +} + +const UnitAddContainer = connect(mapStateToProps, mapDispatchToProps)(UnitAddComponent) + +export default UnitAddContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js new file mode 100644 index 00000000..f334f9f2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitContainer.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import { deleteUnit } from '../../../../../actions/topology/machine' +import UnitComponent from '../../../../../components/app/sidebars/topology/machine/UnitComponent' + +const mapStateToProps = (state, ownProps) => { + return { + unit: state.objects[ownProps.unitType][ownProps.unitId], + index: ownProps.unitId, + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onDelete: () => dispatch(deleteUnit(ownProps.unitType, ownProps.index)), + } +} + +const UnitContainer = connect(mapStateToProps, mapDispatchToProps)(UnitComponent) + +export default UnitContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js new file mode 100644 index 00000000..f382ff74 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitListContainer.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux' +import UnitListComponent from '../../../../../components/app/sidebars/topology/machine/UnitListComponent' + +const mapStateToProps = (state, ownProps) => { + return { + unitIds: + state.objects.machine[ + state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].machineIds[ + state.interactionLevel.position - 1 + ] + ][ownProps.unitType + 'Ids'], + } +} + +const UnitListContainer = connect(mapStateToProps)(UnitListComponent) + +export default UnitListContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js new file mode 100644 index 00000000..00fe4067 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/machine/UnitTabsContainer.js @@ -0,0 +1,5 @@ +import UnitTabsComponent from '../../../../../components/app/sidebars/topology/machine/UnitTabsComponent' + +const UnitTabsContainer = UnitTabsComponent + +export default UnitTabsContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js new file mode 100644 index 00000000..c941e745 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/AddPrefabContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { addPrefab } from '../../../../../actions/prefabs' +import AddPrefabComponent from '../../../../../components/app/sidebars/topology/rack/AddPrefabComponent' + +const mapDispatchToProps = (dispatch) => { + return { + onClick: () => dispatch(addPrefab('name')), + } +} + +const AddPrefabContainer = connect(undefined, mapDispatchToProps)(AddPrefabComponent) + +export default AddPrefabContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js new file mode 100644 index 00000000..58c3b082 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/BackToRoomContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { goDownOneInteractionLevel } from '../../../../../actions/interaction-level' +import BackToRoomComponent from '../../../../../components/app/sidebars/topology/rack/BackToRoomComponent' + +const mapDispatchToProps = (dispatch) => { + return { + onClick: () => dispatch(goDownOneInteractionLevel()), + } +} + +const BackToRoomContainer = connect(undefined, mapDispatchToProps)(BackToRoomComponent) + +export default BackToRoomContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js new file mode 100644 index 00000000..8229a359 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/DeleteRackContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { openDeleteRackModal } from '../../../../../actions/modals/topology' +import DeleteRackComponent from '../../../../../components/app/sidebars/topology/rack/DeleteRackComponent' + +const mapDispatchToProps = (dispatch) => { + return { + onClick: () => dispatch(openDeleteRackModal()), + } +} + +const DeleteRackContainer = connect(undefined, mapDispatchToProps)(DeleteRackComponent) + +export default DeleteRackContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/EmptySlotContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/EmptySlotContainer.js new file mode 100644 index 00000000..cf341da9 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/EmptySlotContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { addMachine } from '../../../../../actions/topology/rack' +import EmptySlotComponent from '../../../../../components/app/sidebars/topology/rack/EmptySlotComponent' + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onAdd: () => dispatch(addMachine(ownProps.position)), + } +} + +const EmptySlotContainer = connect(undefined, mapDispatchToProps)(EmptySlotComponent) + +export default EmptySlotContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js new file mode 100644 index 00000000..fe12827d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineContainer.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux' +import { goFromRackToMachine } from '../../../../../actions/interaction-level' +import MachineComponent from '../../../../../components/app/sidebars/topology/rack/MachineComponent' + +const mapStateToProps = (state, ownProps) => { + return { + machine: state.objects.machine[ownProps.machineId], + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onClick: () => dispatch(goFromRackToMachine(ownProps.position)), + } +} + +const MachineContainer = connect(mapStateToProps, mapDispatchToProps)(MachineComponent) + +export default MachineContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js new file mode 100644 index 00000000..bc5a285a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/MachineListContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import MachineListComponent from '../../../../../components/app/sidebars/topology/rack/MachineListComponent' + +const mapStateToProps = (state) => { + return { + machineIds: state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].machineIds, + } +} + +const MachineListContainer = connect(mapStateToProps)(MachineListComponent) + +export default MachineListContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js new file mode 100644 index 00000000..504dbc61 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackNameContainer.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux' +import { openEditRackNameModal } from '../../../../../actions/modals/topology' +import RackNameComponent from '../../../../../components/app/sidebars/topology/rack/RackNameComponent' + +const mapStateToProps = (state) => { + return { + rackName: state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].name, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onEdit: () => dispatch(openEditRackNameModal()), + } +} + +const RackNameContainer = connect(mapStateToProps, mapDispatchToProps)(RackNameComponent) + +export default RackNameContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js new file mode 100644 index 00000000..453d7e41 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/rack/RackSidebarContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import RackSidebarComponent from '../../../../../components/app/sidebars/topology/rack/RackSidebarComponent' + +const mapStateToProps = (state) => { + return { + rackId: state.objects.tile[state.interactionLevel.tileId].rackId, + } +} + +const RackSidebarContainer = connect(mapStateToProps)(RackSidebarComponent) + +export default RackSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js new file mode 100644 index 00000000..4c1ab99d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/BackToBuildingContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { goDownOneInteractionLevel } from '../../../../../actions/interaction-level' +import BackToBuildingComponent from '../../../../../components/app/sidebars/topology/room/BackToBuildingComponent' + +const mapDispatchToProps = (dispatch) => { + return { + onClick: () => dispatch(goDownOneInteractionLevel()), + } +} + +const BackToBuildingContainer = connect(undefined, mapDispatchToProps)(BackToBuildingComponent) + +export default BackToBuildingContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js new file mode 100644 index 00000000..636fa5c5 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/DeleteRoomContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { openDeleteRoomModal } from '../../../../../actions/modals/topology' +import DeleteRoomComponent from '../../../../../components/app/sidebars/topology/room/DeleteRoomComponent' + +const mapDispatchToProps = (dispatch) => { + return { + onClick: () => dispatch(openDeleteRoomModal()), + } +} + +const DeleteRoomContainer = connect(undefined, mapDispatchToProps)(DeleteRoomComponent) + +export default DeleteRoomContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js new file mode 100644 index 00000000..d17a45d1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/EditRoomContainer.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import { finishRoomEdit, startRoomEdit } from '../../../../../actions/topology/building' +import EditRoomComponent from '../../../../../components/app/sidebars/topology/room/EditRoomComponent' + +const mapStateToProps = (state) => { + return { + isEditing: state.construction.currentRoomInConstruction !== '-1', + isInRackConstructionMode: state.construction.inRackConstructionMode, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onEdit: () => dispatch(startRoomEdit()), + onFinish: () => dispatch(finishRoomEdit()), + } +} + +const EditRoomContainer = connect(mapStateToProps, mapDispatchToProps)(EditRoomComponent) + +export default EditRoomContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js new file mode 100644 index 00000000..cd8319de --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RackConstructionContainer.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import { startRackConstruction, stopRackConstruction } from '../../../../../actions/topology/room' +import RackConstructionComponent from '../../../../../components/app/sidebars/topology/room/RackConstructionComponent' + +const mapStateToProps = (state) => { + return { + inRackConstructionMode: state.construction.inRackConstructionMode, + isEditingRoom: state.construction.currentRoomInConstruction !== '-1', + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onStart: () => dispatch(startRackConstruction()), + onStop: () => dispatch(stopRackConstruction()), + } +} + +const RackConstructionContainer = connect(mapStateToProps, mapDispatchToProps)(RackConstructionComponent) + +export default RackConstructionContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js new file mode 100644 index 00000000..cab16016 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomNameContainer.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux' +import { openEditRoomNameModal } from '../../../../../actions/modals/topology' +import RoomNameComponent from '../../../../../components/app/sidebars/topology/room/RoomNameComponent' + +const mapStateToProps = (state) => { + return { + roomName: state.objects.room[state.interactionLevel.roomId].name, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onEdit: () => dispatch(openEditRoomNameModal()), + } +} + +const RoomNameContainer = connect(mapStateToProps, mapDispatchToProps)(RoomNameComponent) + +export default RoomNameContainer diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js new file mode 100644 index 00000000..8c3ca8ab --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/topology/room/RoomSidebarContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import RoomSidebarComponent from '../../../../../components/app/sidebars/topology/room/RoomSidebarComponent' + +const mapStateToProps = (state) => { + return { + roomId: state.interactionLevel.roomId, + } +} + +const RoomSidebarContainer = connect(mapStateToProps)(RoomSidebarComponent) + +export default RoomSidebarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/auth/Login.js b/opendc-web/opendc-web-ui/src/containers/auth/Login.js new file mode 100644 index 00000000..2f9726bf --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/auth/Login.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types' +import React from 'react' +import GoogleLogin from 'react-google-login' +import { connect } from 'react-redux' +import { logIn } from '../../actions/auth' + +class LoginContainer extends React.Component { + static propTypes = { + visible: PropTypes.bool.isRequired, + onLogin: PropTypes.func.isRequired, + } + + onAuthResponse(response) { + this.props.onLogin({ + email: response.getBasicProfile().getEmail(), + givenName: response.getBasicProfile().getGivenName(), + familyName: response.getBasicProfile().getFamilyName(), + googleId: response.googleId, + authToken: response.getAuthResponse().id_token, + expiresAt: response.getAuthResponse().expires_at, + }) + } + + onAuthFailure(error) { + console.error(error) + } + + render() { + if (!this.props.visible) { + return <span /> + } + + return ( + <GoogleLogin + clientId={process.env.REACT_APP_OAUTH_CLIENT_ID} + onSuccess={this.onAuthResponse.bind(this)} + onFailure={this.onAuthFailure.bind(this)} + render={(renderProps) => ( + <span onClick={renderProps.onClick} className="login btn btn-primary"> + <span className="fa fa-google" /> Login with Google + </span> + )} + ></GoogleLogin> + ) + } +} + +const mapStateToProps = (state, ownProps) => { + return { + visible: ownProps.visible, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onLogin: (payload) => dispatch(logIn(payload)), + } +} + +const Login = connect(mapStateToProps, mapDispatchToProps)(LoginContainer) + +export default Login diff --git a/opendc-web/opendc-web-ui/src/containers/auth/Logout.js b/opendc-web/opendc-web-ui/src/containers/auth/Logout.js new file mode 100644 index 00000000..22400381 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/auth/Logout.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { logOut } from '../../actions/auth' +import LogoutButton from '../../components/navigation/LogoutButton' + +const mapDispatchToProps = (dispatch) => { + return { + onLogout: () => dispatch(logOut()), + } +} + +const Logout = connect(undefined, mapDispatchToProps)(LogoutButton) + +export default Logout diff --git a/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js b/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js new file mode 100644 index 00000000..06da75ab --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js @@ -0,0 +1,14 @@ +import React from 'react' +import { connect } from 'react-redux' + +const mapStateToProps = (state) => { + return { + text: state.auth.givenName + ' ' + state.auth.familyName, + } +} + +const SpanElement = ({ text }) => <span>{text}</span> + +const ProfileName = connect(mapStateToProps)(SpanElement) + +export default ProfileName diff --git a/opendc-web/opendc-web-ui/src/containers/modals/DeleteMachineModal.js b/opendc-web/opendc-web-ui/src/containers/modals/DeleteMachineModal.js new file mode 100644 index 00000000..f30febdb --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/modals/DeleteMachineModal.js @@ -0,0 +1,35 @@ +import React from 'react' +import { connect } from 'react-redux' +import { closeDeleteMachineModal } from '../../actions/modals/topology' +import { deleteMachine } from '../../actions/topology/machine' +import ConfirmationModal from '../../components/modals/ConfirmationModal' + +const DeleteMachineModalComponent = ({ visible, callback }) => ( + <ConfirmationModal + title="Delete this machine" + message="Are you sure you want to delete this machine?" + show={visible} + callback={callback} + /> +) + +const mapStateToProps = (state) => { + return { + visible: state.modals.deleteMachineModalVisible, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + callback: (isConfirmed) => { + if (isConfirmed) { + dispatch(deleteMachine()) + } + dispatch(closeDeleteMachineModal()) + }, + } +} + +const DeleteMachineModal = connect(mapStateToProps, mapDispatchToProps)(DeleteMachineModalComponent) + +export default DeleteMachineModal diff --git a/opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js b/opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js new file mode 100644 index 00000000..e7c4014d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/modals/DeleteProfileModal.js @@ -0,0 +1,35 @@ +import React from 'react' +import { connect } from 'react-redux' +import { closeDeleteProfileModal } from '../../actions/modals/profile' +import { deleteCurrentUser } from '../../actions/users' +import ConfirmationModal from '../../components/modals/ConfirmationModal' + +const DeleteProfileModalComponent = ({ visible, callback }) => ( + <ConfirmationModal + title="Delete my account" + message="Are you sure you want to delete your OpenDC account?" + show={visible} + callback={callback} + /> +) + +const mapStateToProps = (state) => { + return { + visible: state.modals.deleteProfileModalVisible, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + callback: (isConfirmed) => { + if (isConfirmed) { + dispatch(deleteCurrentUser()) + } + dispatch(closeDeleteProfileModal()) + }, + } +} + +const DeleteProfileModal = connect(mapStateToProps, mapDispatchToProps)(DeleteProfileModalComponent) + +export default DeleteProfileModal diff --git a/opendc-web/opendc-web-ui/src/containers/modals/DeleteRackModal.js b/opendc-web/opendc-web-ui/src/containers/modals/DeleteRackModal.js new file mode 100644 index 00000000..0cb22a7e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/modals/DeleteRackModal.js @@ -0,0 +1,35 @@ +import React from 'react' +import { connect } from 'react-redux' +import { closeDeleteRackModal } from '../../actions/modals/topology' +import { deleteRack } from '../../actions/topology/rack' +import ConfirmationModal from '../../components/modals/ConfirmationModal' + +const DeleteRackModalComponent = ({ visible, callback }) => ( + <ConfirmationModal + title="Delete this rack" + message="Are you sure you want to delete this rack?" + show={visible} + callback={callback} + /> +) + +const mapStateToProps = (state) => { + return { + visible: state.modals.deleteRackModalVisible, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + callback: (isConfirmed) => { + if (isConfirmed) { + dispatch(deleteRack()) + } + dispatch(closeDeleteRackModal()) + }, + } +} + +const DeleteRackModal = connect(mapStateToProps, mapDispatchToProps)(DeleteRackModalComponent) + +export default DeleteRackModal diff --git a/opendc-web/opendc-web-ui/src/containers/modals/DeleteRoomModal.js b/opendc-web/opendc-web-ui/src/containers/modals/DeleteRoomModal.js new file mode 100644 index 00000000..1f6eef92 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/modals/DeleteRoomModal.js @@ -0,0 +1,35 @@ +import React from 'react' +import { connect } from 'react-redux' +import { closeDeleteRoomModal } from '../../actions/modals/topology' +import { deleteRoom } from '../../actions/topology/room' +import ConfirmationModal from '../../components/modals/ConfirmationModal' + +const DeleteRoomModalComponent = ({ visible, callback }) => ( + <ConfirmationModal + title="Delete this room" + message="Are you sure you want to delete this room?" + show={visible} + callback={callback} + /> +) + +const mapStateToProps = (state) => { + return { + visible: state.modals.deleteRoomModalVisible, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + callback: (isConfirmed) => { + if (isConfirmed) { + dispatch(deleteRoom()) + } + dispatch(closeDeleteRoomModal()) + }, + } +} + +const DeleteRoomModal = connect(mapStateToProps, mapDispatchToProps)(DeleteRoomModalComponent) + +export default DeleteRoomModal diff --git a/opendc-web/opendc-web-ui/src/containers/modals/EditRackNameModal.js b/opendc-web/opendc-web-ui/src/containers/modals/EditRackNameModal.js new file mode 100644 index 00000000..9128f449 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/modals/EditRackNameModal.js @@ -0,0 +1,40 @@ +import React from 'react' +import { connect } from 'react-redux' +import { closeEditRackNameModal } from '../../actions/modals/topology' +import { editRackName } from '../../actions/topology/rack' +import TextInputModal from '../../components/modals/TextInputModal' + +const EditRackNameModalComponent = ({ visible, previousName, callback }) => ( + <TextInputModal + title="Edit rack name" + label="Rack name" + show={visible} + initialValue={previousName} + callback={callback} + /> +) + +const mapStateToProps = (state) => { + return { + visible: state.modals.editRackNameModalVisible, + previousName: + state.interactionLevel.mode === 'RACK' + ? state.objects.rack[state.objects.tile[state.interactionLevel.tileId].rackId].name + : '', + } +} + +const mapDispatchToProps = (dispatch) => { + return { + callback: (name) => { + if (name) { + dispatch(editRackName(name)) + } + dispatch(closeEditRackNameModal()) + }, + } +} + +const EditRackNameModal = connect(mapStateToProps, mapDispatchToProps)(EditRackNameModalComponent) + +export default EditRackNameModal diff --git a/opendc-web/opendc-web-ui/src/containers/modals/EditRoomNameModal.js b/opendc-web/opendc-web-ui/src/containers/modals/EditRoomNameModal.js new file mode 100644 index 00000000..8032a5d1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/modals/EditRoomNameModal.js @@ -0,0 +1,38 @@ +import React from 'react' +import { connect } from 'react-redux' +import { closeEditRoomNameModal } from '../../actions/modals/topology' +import { editRoomName } from '../../actions/topology/room' +import TextInputModal from '../../components/modals/TextInputModal' + +const EditRoomNameModalComponent = ({ visible, previousName, callback }) => ( + <TextInputModal + title="Edit room name" + label="Room name" + show={visible} + initialValue={previousName} + callback={callback} + /> +) + +const mapStateToProps = (state) => { + return { + visible: state.modals.editRoomNameModalVisible, + previousName: + state.interactionLevel.mode === 'ROOM' ? state.objects.room[state.interactionLevel.roomId].name : '', + } +} + +const mapDispatchToProps = (dispatch) => { + return { + callback: (name) => { + if (name) { + dispatch(editRoomName(name)) + } + dispatch(closeEditRoomNameModal()) + }, + } +} + +const EditRoomNameModal = connect(mapStateToProps, mapDispatchToProps)(EditRoomNameModalComponent) + +export default EditRoomNameModal diff --git a/opendc-web/opendc-web-ui/src/containers/modals/NewPortfolioModal.js b/opendc-web/opendc-web-ui/src/containers/modals/NewPortfolioModal.js new file mode 100644 index 00000000..6cf12d8e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/modals/NewPortfolioModal.js @@ -0,0 +1,30 @@ +import { connect } from 'react-redux' +import NewPortfolioModalComponent from '../../components/modals/custom-components/NewPortfolioModalComponent' +import { addPortfolio } from '../../actions/portfolios' +import { closeNewPortfolioModal } from '../../actions/modals/portfolios' + +const mapStateToProps = (state) => { + return { + show: state.modals.newPortfolioModalVisible, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + callback: (name, targets) => { + if (name) { + dispatch( + addPortfolio({ + name, + targets, + }) + ) + } + dispatch(closeNewPortfolioModal()) + }, + } +} + +const NewPortfolioModal = connect(mapStateToProps, mapDispatchToProps)(NewPortfolioModalComponent) + +export default NewPortfolioModal diff --git a/opendc-web/opendc-web-ui/src/containers/modals/NewProjectModal.js b/opendc-web/opendc-web-ui/src/containers/modals/NewProjectModal.js new file mode 100644 index 00000000..d306dc45 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/modals/NewProjectModal.js @@ -0,0 +1,30 @@ +import React from 'react' +import { connect } from 'react-redux' +import { closeNewProjectModal } from '../../actions/modals/projects' +import { addProject } from '../../actions/projects' +import TextInputModal from '../../components/modals/TextInputModal' + +const NewProjectModalComponent = ({ visible, callback }) => ( + <TextInputModal title="New Project" label="Project title" show={visible} callback={callback} /> +) + +const mapStateToProps = (state) => { + return { + visible: state.modals.newProjectModalVisible, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + callback: (text) => { + if (text) { + dispatch(addProject(text)) + } + dispatch(closeNewProjectModal()) + }, + } +} + +const NewProjectModal = connect(mapStateToProps, mapDispatchToProps)(NewProjectModalComponent) + +export default NewProjectModal diff --git a/opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js b/opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js new file mode 100644 index 00000000..7d774fa4 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/modals/NewScenarioModal.js @@ -0,0 +1,50 @@ +import { connect } from 'react-redux' +import NewScenarioModalComponent from '../../components/modals/custom-components/NewScenarioModalComponent' +import { addScenario } from '../../actions/scenarios' +import { closeNewScenarioModal } from '../../actions/modals/scenarios' + +const mapStateToProps = (state) => { + let topologies = + state.currentProjectId !== '-1' + ? state.objects.project[state.currentProjectId].topologyIds.map((t) => state.objects.topology[t]) + : [] + if (topologies.filter((t) => !t).length > 0) { + topologies = [] + } + + return { + show: state.modals.newScenarioModalVisible, + currentPortfolioId: state.currentPortfolioId, + currentPortfolioScenarioIds: + state.currentPortfolioId !== '-1' && state.objects.portfolio[state.currentPortfolioId] + ? state.objects.portfolio[state.currentPortfolioId].scenarioIds + : [], + traces: Object.values(state.objects.trace), + topologies, + schedulers: Object.values(state.objects.scheduler), + } +} + +const mapDispatchToProps = (dispatch) => { + return { + callback: (name, portfolioId, trace, topology, operational) => { + if (name) { + dispatch( + addScenario({ + portfolioId, + name, + trace, + topology, + operational, + }) + ) + } + + dispatch(closeNewScenarioModal()) + }, + } +} + +const NewScenarioModal = connect(mapStateToProps, mapDispatchToProps)(NewScenarioModalComponent) + +export default NewScenarioModal diff --git a/opendc-web/opendc-web-ui/src/containers/modals/NewTopologyModal.js b/opendc-web/opendc-web-ui/src/containers/modals/NewTopologyModal.js new file mode 100644 index 00000000..0acf6cf2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/modals/NewTopologyModal.js @@ -0,0 +1,42 @@ +import { connect } from 'react-redux' +import NewTopologyModalComponent from '../../components/modals/custom-components/NewTopologyModalComponent' +import { closeNewTopologyModal } from '../../actions/modals/topology' +import { addTopology } from '../../actions/topologies' + +const mapStateToProps = (state) => { + let topologies = state.objects.project[state.currentProjectId] + ? state.objects.project[state.currentProjectId].topologyIds.map((t) => state.objects.topology[t]) + : [] + if (topologies.filter((t) => !t).length > 0) { + topologies = [] + } + + return { + show: state.modals.changeTopologyModalVisible, + topologies, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onCreateTopology: (name) => { + if (name) { + dispatch(addTopology(name, undefined)) + } + dispatch(closeNewTopologyModal()) + }, + onDuplicateTopology: (name, id) => { + if (name) { + dispatch(addTopology(name, id)) + } + dispatch(closeNewTopologyModal()) + }, + onCancel: () => { + dispatch(closeNewTopologyModal()) + }, + } +} + +const NewTopologyModal = connect(mapStateToProps, mapDispatchToProps)(NewTopologyModalComponent) + +export default NewTopologyModal diff --git a/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js b/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js new file mode 100644 index 00000000..845d54e1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/navigation/AppNavbarContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import AppNavbarComponent from '../../components/navigation/AppNavbarComponent' + +const mapStateToProps = (state) => { + return { + project: state.currentProjectId !== '-1' ? state.objects.project[state.currentProjectId] : undefined, + } +} + +const AppNavbarContainer = connect(mapStateToProps)(AppNavbarComponent) + +export default AppNavbarContainer diff --git a/opendc-web/opendc-web-ui/src/containers/projects/FilterLink.js b/opendc-web/opendc-web-ui/src/containers/projects/FilterLink.js new file mode 100644 index 00000000..dfd6affe --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/projects/FilterLink.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux' +import { setAuthVisibilityFilter } from '../../actions/projects' +import FilterButton from '../../components/projects/FilterButton' + +const mapStateToProps = (state, ownProps) => { + return { + active: state.projectList.authVisibilityFilter === ownProps.filter, + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onClick: () => dispatch(setAuthVisibilityFilter(ownProps.filter)), + } +} + +const FilterLink = connect(mapStateToProps, mapDispatchToProps)(FilterButton) + +export default FilterLink diff --git a/opendc-web/opendc-web-ui/src/containers/projects/NewProjectButtonContainer.js b/opendc-web/opendc-web-ui/src/containers/projects/NewProjectButtonContainer.js new file mode 100644 index 00000000..ffd4a4a3 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/projects/NewProjectButtonContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import { openNewProjectModal } from '../../actions/modals/projects' +import NewProjectButtonComponent from '../../components/projects/NewProjectButtonComponent' + +const mapDispatchToProps = (dispatch) => { + return { + onClick: () => dispatch(openNewProjectModal()), + } +} + +const NewProjectButtonContainer = connect(undefined, mapDispatchToProps)(NewProjectButtonComponent) + +export default NewProjectButtonContainer diff --git a/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js b/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js new file mode 100644 index 00000000..8bcbb7fd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/projects/ProjectActions.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import { deleteProject } from '../../actions/projects' +import ProjectActionButtons from '../../components/projects/ProjectActionButtons' + +const mapStateToProps = (state, ownProps) => { + return { + projectId: ownProps.projectId, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onViewUsers: (id) => {}, // TODO implement user viewing + onDelete: (id) => dispatch(deleteProject(id)), + } +} + +const ProjectActions = connect(mapStateToProps, mapDispatchToProps)(ProjectActionButtons) + +export default ProjectActions diff --git a/opendc-web/opendc-web-ui/src/containers/projects/VisibleProjectAuthList.js b/opendc-web/opendc-web-ui/src/containers/projects/VisibleProjectAuthList.js new file mode 100644 index 00000000..f0010540 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/projects/VisibleProjectAuthList.js @@ -0,0 +1,32 @@ +import { connect } from 'react-redux' +import ProjectList from '../../components/projects/ProjectAuthList' + +const getVisibleProjectAuths = (projectAuths, filter) => { + switch (filter) { + case 'SHOW_ALL': + return projectAuths + case 'SHOW_OWN': + return projectAuths.filter((projectAuth) => projectAuth.authorizationLevel === 'OWN') + case 'SHOW_SHARED': + return projectAuths.filter((projectAuth) => projectAuth.authorizationLevel !== 'OWN') + default: + return projectAuths + } +} + +const mapStateToProps = (state) => { + const denormalizedAuthorizations = state.projectList.authorizationsOfCurrentUser.map((authorizationIds) => { + const authorization = state.objects.authorization[authorizationIds] + authorization.user = state.objects.user[authorization.userId] + authorization.project = state.objects.project[authorization.projectId] + return authorization + }) + + return { + authorizations: getVisibleProjectAuths(denormalizedAuthorizations, state.projectList.authVisibilityFilter), + } +} + +const VisibleProjectAuthList = connect(mapStateToProps)(ProjectList) + +export default VisibleProjectAuthList diff --git a/opendc-web/opendc-web-ui/src/index.js b/opendc-web/opendc-web-ui/src/index.js new file mode 100644 index 00000000..3517147e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/index.js @@ -0,0 +1,30 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import * as Sentry from '@sentry/react' +import { Integrations } from '@sentry/tracing' +import { Provider } from 'react-redux' +import { setupSocketConnection } from './api/socket' +import './index.sass' +import Routes from './routes' +import configureStore from './store/configure-store' + +setupSocketConnection(() => { + const store = configureStore() + + // Initialize Sentry if the user has configured a DSN + if (process.env.REACT_APP_SENTRY_DSN) { + Sentry.init({ + environment: process.env.NODE_ENV, + dsn: process.env.REACT_APP_SENTRY_DSN, + integrations: [new Integrations.BrowserTracing()], + tracesSampleRate: 0.1, + }) + } + + ReactDOM.render( + <Provider store={store}> + <Routes /> + </Provider>, + document.getElementById('root') + ) +}) diff --git a/opendc-web/opendc-web-ui/src/index.sass b/opendc-web/opendc-web-ui/src/index.sass new file mode 100644 index 00000000..a78f7a19 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/index.sass @@ -0,0 +1,52 @@ +@import "~bootstrap/scss/bootstrap" + +@import ./style-globals/_mixins.sass +@import ./style-globals/_variables.sass + +html, body, #root + margin: 0 + padding: 0 + width: 100% + height: 100% + + font-family: Roboto, Helvetica, Verdana, sans-serif + background: #eee + + // Scroll padding for top navbar + scroll-padding-top: 60px + +.full-height + position: relative + height: 100% !important + +.page-container + padding-top: 60px + +.text-page-container + padding-top: 80px + display: flex + flex-flow: column + +.vertically-expanding-container + flex: 1 1 auto + overflow-y: auto + +.bottom-btn-container + flex: 0 1 auto + padding: 20px 0 + +.btn, .list-group-item-action, .clickable + +clickable + +.btn-circle + +border-radius(50%) + +a, a:hover + text-decoration: none + +.app-page-container + padding-left: $side-bar-width + padding-top: 15px + +.w-70 + width: 70% !important diff --git a/opendc-web/opendc-web-ui/src/pages/App.js b/opendc-web/opendc-web-ui/src/pages/App.js new file mode 100644 index 00000000..cbc805b8 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/pages/App.js @@ -0,0 +1,137 @@ +import PropTypes from 'prop-types' +import React from 'react' +import DocumentTitle from 'react-document-title' +import { connect } from 'react-redux' +import { ShortcutManager } from 'react-shortcuts' +import { openPortfolioSucceeded } from '../actions/portfolios' +import { openProjectSucceeded } from '../actions/projects' +import ToolPanelComponent from '../components/app/map/controls/ToolPanelComponent' +import LoadingScreen from '../components/app/map/LoadingScreen' +import ScaleIndicatorContainer from '../containers/app/map/controls/ScaleIndicatorContainer' +import MapStage from '../containers/app/map/MapStage' +import TopologySidebarContainer from '../containers/app/sidebars/topology/TopologySidebarContainer' +import DeleteMachineModal from '../containers/modals/DeleteMachineModal' +import DeleteRackModal from '../containers/modals/DeleteRackModal' +import DeleteRoomModal from '../containers/modals/DeleteRoomModal' +import EditRackNameModal from '../containers/modals/EditRackNameModal' +import EditRoomNameModal from '../containers/modals/EditRoomNameModal' +import KeymapConfiguration from '../shortcuts/keymap' +import NewTopologyModal from '../containers/modals/NewTopologyModal' +import AppNavbarContainer from '../containers/navigation/AppNavbarContainer' +import ProjectSidebarContainer from '../containers/app/sidebars/project/ProjectSidebarContainer' +import { openScenarioSucceeded } from '../actions/scenarios' +import NewPortfolioModal from '../containers/modals/NewPortfolioModal' +import NewScenarioModal from '../containers/modals/NewScenarioModal' +import PortfolioResultsContainer from '../containers/app/results/PortfolioResultsContainer' + +const shortcutManager = new ShortcutManager(KeymapConfiguration) + +class AppComponent extends React.Component { + static propTypes = { + projectId: PropTypes.string.isRequired, + portfolioId: PropTypes.string, + scenarioId: PropTypes.string, + projectName: PropTypes.string, + } + static childContextTypes = { + shortcuts: PropTypes.object.isRequired, + } + + componentDidMount() { + if (this.props.scenarioId) { + this.props.openScenarioSucceeded(this.props.projectId, this.props.portfolioId, this.props.scenarioId) + } else if (this.props.portfolioId) { + this.props.openPortfolioSucceeded(this.props.projectId, this.props.portfolioId) + } else { + this.props.openProjectSucceeded(this.props.projectId) + } + } + + getChildContext() { + return { + shortcuts: shortcutManager, + } + } + + render() { + const constructionElements = this.props.topologyIsLoading ? ( + <div className="full-height d-flex align-items-center justify-content-center"> + <LoadingScreen /> + </div> + ) : ( + <div className="full-height"> + <MapStage /> + <ScaleIndicatorContainer /> + <ToolPanelComponent /> + <ProjectSidebarContainer /> + <TopologySidebarContainer /> + </div> + ) + + const portfolioElements = ( + <div className="full-height app-page-container"> + <ProjectSidebarContainer /> + <div className="container-fluid full-height"> + <PortfolioResultsContainer /> + </div> + </div> + ) + + const scenarioElements = ( + <div className="full-height app-page-container"> + <ProjectSidebarContainer /> + <div className="container-fluid full-height"> + <h2>Scenario loading</h2> + </div> + </div> + ) + + return ( + <DocumentTitle + title={this.props.projectName ? this.props.projectName + ' - OpenDC' : 'Simulation - OpenDC'} + > + <div className="page-container full-height"> + <AppNavbarContainer fullWidth={true} /> + {this.props.scenarioId + ? scenarioElements + : this.props.portfolioId + ? portfolioElements + : constructionElements} + <NewTopologyModal /> + <NewPortfolioModal /> + <NewScenarioModal /> + <EditRoomNameModal /> + <DeleteRoomModal /> + <EditRackNameModal /> + <DeleteRackModal /> + <DeleteMachineModal /> + </div> + </DocumentTitle> + ) + } +} + +const mapStateToProps = (state) => { + let projectName = undefined + if (state.currentProjectId !== '-1' && state.objects.project[state.currentProjectId]) { + projectName = state.objects.project[state.currentProjectId].name + } + + return { + topologyIsLoading: state.currentTopologyId === '-1', + projectName, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + openProjectSucceeded: (projectId) => dispatch(openProjectSucceeded(projectId)), + openPortfolioSucceeded: (projectId, portfolioId) => dispatch(openPortfolioSucceeded(projectId, portfolioId)), + openScenarioSucceeded: (projectId, portfolioId, scenarioId) => + dispatch(openScenarioSucceeded(projectId, portfolioId, scenarioId)), + } +} + +const App = connect(mapStateToProps, mapDispatchToProps)(AppComponent) + +export default App diff --git a/opendc-web/opendc-web-ui/src/pages/Home.js b/opendc-web/opendc-web-ui/src/pages/Home.js new file mode 100644 index 00000000..6fc940c0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/pages/Home.js @@ -0,0 +1,33 @@ +import React from 'react' +import DocumentTitle from 'react-document-title' +import ContactSection from '../components/home/ContactSection' +import IntroSection from '../components/home/IntroSection' +import JumbotronHeader from '../components/home/JumbotronHeader' +import ModelingSection from '../components/home/ModelingSection' +import SimulationSection from '../components/home/SimulationSection' +import StakeholderSection from '../components/home/StakeholderSection' +import TeamSection from '../components/home/TeamSection' +import TechnologiesSection from '../components/home/TechnologiesSection' +import HomeNavbar from '../components/navigation/HomeNavbar' +import './Home.sass' + +function Home() { + return ( + <div> + <HomeNavbar /> + <div className="body-wrapper page-container"> + <JumbotronHeader /> + <IntroSection /> + <StakeholderSection /> + <ModelingSection /> + <SimulationSection /> + <TechnologiesSection /> + <TeamSection /> + <ContactSection /> + <DocumentTitle title="OpenDC" /> + </div> + </div> + ) +} + +export default Home diff --git a/opendc-web/opendc-web-ui/src/pages/Home.sass b/opendc-web/opendc-web-ui/src/pages/Home.sass new file mode 100644 index 00000000..79cb9698 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/pages/Home.sass @@ -0,0 +1,9 @@ +.body-wrapper + position: relative + overflow-y: hidden + +.intro-section, .modeling-section, .technologies-section + background-color: #fff + +.stakeholder-section, .simulation-section, .team-section + background-color: #f2f2f2 diff --git a/opendc-web/opendc-web-ui/src/pages/NotFound.js b/opendc-web/opendc-web-ui/src/pages/NotFound.js new file mode 100644 index 00000000..72be7342 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/pages/NotFound.js @@ -0,0 +1,14 @@ +import React from 'react' +import DocumentTitle from 'react-document-title' +import TerminalWindow from '../components/not-found/TerminalWindow' +import './NotFound.sass' + +const NotFound = () => ( + <DocumentTitle title="Page Not Found - OpenDC"> + <div className="not-found-backdrop"> + <TerminalWindow /> + </div> + </DocumentTitle> +) + +export default NotFound diff --git a/opendc-web/opendc-web-ui/src/pages/NotFound.sass b/opendc-web/opendc-web-ui/src/pages/NotFound.sass new file mode 100644 index 00000000..59231f7a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/pages/NotFound.sass @@ -0,0 +1,11 @@ +.not-found-backdrop + position: absolute + left: 0 + top: 0 + + margin: 0 + padding: 0 + width: 100% + height: 100% + + background-image: linear-gradient(135deg, #00678a, #008fbf, #00A6D6) diff --git a/opendc-web/opendc-web-ui/src/pages/Profile.js b/opendc-web/opendc-web-ui/src/pages/Profile.js new file mode 100644 index 00000000..0d94b519 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/pages/Profile.js @@ -0,0 +1,35 @@ +import React from 'react' +import DocumentTitle from 'react-document-title' +import { connect } from 'react-redux' +import { openDeleteProfileModal } from '../actions/modals/profile' +import DeleteProfileModal from '../containers/modals/DeleteProfileModal' +import AppNavbarContainer from '../containers/navigation/AppNavbarContainer' + +const ProfileContainer = ({ onDelete }) => ( + <DocumentTitle title="My Profile - OpenDC"> + <div className="full-height"> + <AppNavbarContainer fullWidth={false} /> + <div className="container text-page-container full-height"> + <button className="btn btn-danger mb-2 ml-auto mr-auto" style={{ maxWidth: 300 }} onClick={onDelete}> + Delete my account on OpenDC + </button> + <p className="text-muted text-center"> + This does not delete your Google account, but simply disconnects it from the OpenDC platform and + deletes any project info that is associated with you (projects you own and any authorizations you + may have on other projects). + </p> + </div> + <DeleteProfileModal /> + </div> + </DocumentTitle> +) + +const mapDispatchToProps = (dispatch) => { + return { + onDelete: () => dispatch(openDeleteProfileModal()), + } +} + +const Profile = connect(undefined, mapDispatchToProps)(ProfileContainer) + +export default Profile diff --git a/opendc-web/opendc-web-ui/src/pages/Projects.js b/opendc-web/opendc-web-ui/src/pages/Projects.js new file mode 100644 index 00000000..bb54aaa5 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/pages/Projects.js @@ -0,0 +1,43 @@ +import React from 'react' +import DocumentTitle from 'react-document-title' +import { connect } from 'react-redux' +import { openNewProjectModal } from '../actions/modals/projects' +import { fetchAuthorizationsOfCurrentUser } from '../actions/users' +import ProjectFilterPanel from '../components/projects/FilterPanel' +import NewProjectModal from '../containers/modals/NewProjectModal' +import NewProjectButtonContainer from '../containers/projects/NewProjectButtonContainer' +import VisibleProjectList from '../containers/projects/VisibleProjectAuthList' +import AppNavbarContainer from '../containers/navigation/AppNavbarContainer' + +class ProjectsContainer extends React.Component { + componentDidMount() { + this.props.fetchAuthorizationsOfCurrentUser() + } + + render() { + return ( + <DocumentTitle title="My Projects - OpenDC"> + <div className="full-height"> + <AppNavbarContainer fullWidth={false} /> + <div className="container text-page-container full-height"> + <ProjectFilterPanel /> + <VisibleProjectList /> + <NewProjectButtonContainer /> + </div> + <NewProjectModal /> + </div> + </DocumentTitle> + ) + } +} + +const mapDispatchToProps = (dispatch) => { + return { + fetchAuthorizationsOfCurrentUser: () => dispatch(fetchAuthorizationsOfCurrentUser()), + openNewProjectModal: () => dispatch(openNewProjectModal()), + } +} + +const Projects = connect(undefined, mapDispatchToProps)(ProjectsContainer) + +export default Projects diff --git a/opendc-web/opendc-web-ui/src/reducers/auth.js b/opendc-web/opendc-web-ui/src/reducers/auth.js new file mode 100644 index 00000000..399a4b10 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/reducers/construction-mode.js b/opendc-web/opendc-web-ui/src/reducers/construction-mode.js new file mode 100644 index 00000000..257dddd2 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/reducers/current-ids.js b/opendc-web/opendc-web-ui/src/reducers/current-ids.js new file mode 100644 index 00000000..9b46aa60 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/reducers/index.js b/opendc-web/opendc-web-ui/src/reducers/index.js new file mode 100644 index 00000000..787d5a74 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/reducers/index.js @@ -0,0 +1,25 @@ +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 { modals } from './modals' +import { objects } from './objects' +import { projectList } from './project-list' + +const rootReducer = combineReducers({ + objects, + modals, + projectList, + construction, + map, + currentProjectId, + currentTopologyId, + currentPortfolioId, + currentScenarioId, + interactionLevel, + auth, +}) + +export default rootReducer diff --git a/opendc-web/opendc-web-ui/src/reducers/interaction-level.js b/opendc-web/opendc-web-ui/src/reducers/interaction-level.js new file mode 100644 index 00000000..eafcb269 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/reducers/map.js b/opendc-web/opendc-web-ui/src/reducers/map.js new file mode 100644 index 00000000..de712c15 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/reducers/modals.js b/opendc-web/opendc-web-ui/src/reducers/modals.js new file mode 100644 index 00000000..a7656373 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/reducers/modals.js @@ -0,0 +1,45 @@ +import { combineReducers } from 'redux' +import { CLOSE_DELETE_PROFILE_MODAL, OPEN_DELETE_PROFILE_MODAL } from '../actions/modals/profile' +import { CLOSE_NEW_PROJECT_MODAL, OPEN_NEW_PROJECT_MODAL } from '../actions/modals/projects' +import { + CLOSE_NEW_TOPOLOGY_MODAL, + CLOSE_DELETE_MACHINE_MODAL, + CLOSE_DELETE_RACK_MODAL, + CLOSE_DELETE_ROOM_MODAL, + CLOSE_EDIT_RACK_NAME_MODAL, + CLOSE_EDIT_ROOM_NAME_MODAL, + OPEN_NEW_TOPOLOGY_MODAL, + OPEN_DELETE_MACHINE_MODAL, + OPEN_DELETE_RACK_MODAL, + OPEN_DELETE_ROOM_MODAL, + OPEN_EDIT_RACK_NAME_MODAL, + OPEN_EDIT_ROOM_NAME_MODAL, +} from '../actions/modals/topology' +import { CLOSE_NEW_PORTFOLIO_MODAL, OPEN_NEW_PORTFOLIO_MODAL } from '../actions/modals/portfolios' +import { CLOSE_NEW_SCENARIO_MODAL, OPEN_NEW_SCENARIO_MODAL } from '../actions/modals/scenarios' + +function modal(openAction, closeAction) { + return function (state = false, action) { + switch (action.type) { + case openAction: + return true + case closeAction: + return false + default: + return state + } + } +} + +export const modals = combineReducers({ + newProjectModalVisible: modal(OPEN_NEW_PROJECT_MODAL, CLOSE_NEW_PROJECT_MODAL), + deleteProfileModalVisible: modal(OPEN_DELETE_PROFILE_MODAL, CLOSE_DELETE_PROFILE_MODAL), + changeTopologyModalVisible: modal(OPEN_NEW_TOPOLOGY_MODAL, CLOSE_NEW_TOPOLOGY_MODAL), + editRoomNameModalVisible: modal(OPEN_EDIT_ROOM_NAME_MODAL, CLOSE_EDIT_ROOM_NAME_MODAL), + deleteRoomModalVisible: modal(OPEN_DELETE_ROOM_MODAL, CLOSE_DELETE_ROOM_MODAL), + editRackNameModalVisible: modal(OPEN_EDIT_RACK_NAME_MODAL, CLOSE_EDIT_RACK_NAME_MODAL), + deleteRackModalVisible: modal(OPEN_DELETE_RACK_MODAL, CLOSE_DELETE_RACK_MODAL), + deleteMachineModalVisible: modal(OPEN_DELETE_MACHINE_MODAL, CLOSE_DELETE_MACHINE_MODAL), + newPortfolioModalVisible: modal(OPEN_NEW_PORTFOLIO_MODAL, CLOSE_NEW_PORTFOLIO_MODAL), + newScenarioModalVisible: modal(OPEN_NEW_SCENARIO_MODAL, CLOSE_NEW_SCENARIO_MODAL), +}) diff --git a/opendc-web/opendc-web-ui/src/reducers/objects.js b/opendc-web/opendc-web-ui/src/reducers/objects.js new file mode 100644 index 00000000..1f721b2e --- /dev/null +++ b/opendc-web/opendc-web-ui/src/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/reducers/project-list.js b/opendc-web/opendc-web-ui/src/reducers/project-list.js new file mode 100644 index 00000000..1f1aa8d0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/reducers/project-list.js @@ -0,0 +1,30 @@ +import { combineReducers } from 'redux' +import { ADD_PROJECT_SUCCEEDED, DELETE_PROJECT_SUCCEEDED, SET_AUTH_VISIBILITY_FILTER } 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 function authVisibilityFilter(state = 'SHOW_ALL', action) { + switch (action.type) { + case SET_AUTH_VISIBILITY_FILTER: + return action.filter + default: + return state + } +} + +export const projectList = combineReducers({ + authorizationsOfCurrentUser, + authVisibilityFilter, +}) diff --git a/opendc-web/opendc-web-ui/src/routes/index.js b/opendc-web/opendc-web-ui/src/routes/index.js new file mode 100644 index 00000000..4291a046 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/routes/index.js @@ -0,0 +1,40 @@ +import React from 'react' +import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom' +import { userIsLoggedIn } from '../auth/index' +import App from '../pages/App' +import Home from '../pages/Home' +import NotFound from '../pages/NotFound' +import Profile from '../pages/Profile' +import Projects from '../pages/Projects' + +const ProtectedComponent = (component) => () => (userIsLoggedIn() ? component : <Redirect to="/" />) +const AppComponent = ({ match }) => + userIsLoggedIn() ? ( + <App + projectId={match.params.projectId} + portfolioId={match.params.portfolioId} + scenarioId={match.params.scenarioId} + /> + ) : ( + <Redirect to="/" /> + ) + +const Routes = () => ( + <BrowserRouter> + <Switch> + <Route exact path="/" component={Home} /> + <Route exact path="/projects" render={ProtectedComponent(<Projects />)} /> + <Route exact path="/projects/:projectId" component={AppComponent} /> + <Route exact path="/projects/:projectId/portfolios/:portfolioId" component={AppComponent} /> + <Route + exact + path="/projects/:projectId/portfolios/:portfolioId/scenarios/:scenarioId" + component={AppComponent} + /> + <Route exact path="/profile" render={ProtectedComponent(<Profile />)} /> + <Route path="/*" component={NotFound} /> + </Switch> + </BrowserRouter> +) + +export default Routes diff --git a/opendc-web/opendc-web-ui/src/sagas/index.js b/opendc-web/opendc-web-ui/src/sagas/index.js new file mode 100644 index 00000000..6332b2fb --- /dev/null +++ b/opendc-web/opendc-web-ui/src/sagas/index.js @@ -0,0 +1,80 @@ +import { takeEvery } from 'redux-saga/effects' +import { LOG_IN } from '../actions/auth' +import { ADD_PORTFOLIO, DELETE_PORTFOLIO, OPEN_PORTFOLIO_SUCCEEDED, UPDATE_PORTFOLIO } from '../actions/portfolios' +import { ADD_PROJECT, DELETE_PROJECT, OPEN_PROJECT_SUCCEEDED } from '../actions/projects' +import { + ADD_TILE, + CANCEL_NEW_ROOM_CONSTRUCTION, + DELETE_TILE, + START_NEW_ROOM_CONSTRUCTION, +} from '../actions/topology/building' +import { ADD_UNIT, DELETE_MACHINE, DELETE_UNIT } from '../actions/topology/machine' +import { ADD_MACHINE, DELETE_RACK, EDIT_RACK_NAME } from '../actions/topology/rack' +import { ADD_RACK_TO_TILE, DELETE_ROOM, EDIT_ROOM_NAME } from '../actions/topology/room' +import { DELETE_CURRENT_USER, FETCH_AUTHORIZATIONS_OF_CURRENT_USER } from '../actions/users' +import { onAddPortfolio, onDeletePortfolio, onOpenPortfolioSucceeded, onUpdatePortfolio } from './portfolios' +import { onDeleteCurrentUser } from './profile' +import { onOpenProjectSucceeded, onProjectAdd, onProjectDelete } from './projects' +import { + onAddMachine, + onAddRackToTile, + onAddTile, + onAddTopology, + onAddUnit, + onCancelNewRoomConstruction, + onDeleteMachine, + onDeleteRack, + onDeleteRoom, + onDeleteTile, + onDeleteTopology, + onDeleteUnit, + onEditRackName, + onEditRoomName, + onStartNewRoomConstruction, +} from './topology' +import { onFetchAuthorizationsOfCurrentUser, onFetchLoggedInUser } from './users' +import { ADD_TOPOLOGY, DELETE_TOPOLOGY } from '../actions/topologies' +import { ADD_SCENARIO, DELETE_SCENARIO, OPEN_SCENARIO_SUCCEEDED, UPDATE_SCENARIO } from '../actions/scenarios' +import { onAddScenario, onDeleteScenario, onOpenScenarioSucceeded, onUpdateScenario } from './scenarios' +import { onAddPrefab } from './prefabs' +import { ADD_PREFAB } from '../actions/prefabs' + +export default function* rootSaga() { + yield takeEvery(LOG_IN, onFetchLoggedInUser) + + yield takeEvery(FETCH_AUTHORIZATIONS_OF_CURRENT_USER, onFetchAuthorizationsOfCurrentUser) + yield takeEvery(ADD_PROJECT, onProjectAdd) + yield takeEvery(DELETE_PROJECT, onProjectDelete) + + yield takeEvery(DELETE_CURRENT_USER, onDeleteCurrentUser) + + yield takeEvery(OPEN_PROJECT_SUCCEEDED, onOpenProjectSucceeded) + yield takeEvery(OPEN_PORTFOLIO_SUCCEEDED, onOpenPortfolioSucceeded) + yield takeEvery(OPEN_SCENARIO_SUCCEEDED, onOpenScenarioSucceeded) + + yield takeEvery(ADD_TOPOLOGY, onAddTopology) + yield takeEvery(DELETE_TOPOLOGY, onDeleteTopology) + yield takeEvery(START_NEW_ROOM_CONSTRUCTION, onStartNewRoomConstruction) + yield takeEvery(CANCEL_NEW_ROOM_CONSTRUCTION, onCancelNewRoomConstruction) + yield takeEvery(ADD_TILE, onAddTile) + yield takeEvery(DELETE_TILE, onDeleteTile) + yield takeEvery(EDIT_ROOM_NAME, onEditRoomName) + yield takeEvery(DELETE_ROOM, onDeleteRoom) + yield takeEvery(EDIT_RACK_NAME, onEditRackName) + yield takeEvery(DELETE_RACK, onDeleteRack) + yield takeEvery(ADD_RACK_TO_TILE, onAddRackToTile) + yield takeEvery(ADD_MACHINE, onAddMachine) + yield takeEvery(DELETE_MACHINE, onDeleteMachine) + yield takeEvery(ADD_UNIT, onAddUnit) + yield takeEvery(DELETE_UNIT, onDeleteUnit) + + yield takeEvery(ADD_PORTFOLIO, onAddPortfolio) + yield takeEvery(UPDATE_PORTFOLIO, onUpdatePortfolio) + yield takeEvery(DELETE_PORTFOLIO, onDeletePortfolio) + + yield takeEvery(ADD_SCENARIO, onAddScenario) + yield takeEvery(UPDATE_SCENARIO, onUpdateScenario) + yield takeEvery(DELETE_SCENARIO, onDeleteScenario) + + yield takeEvery(ADD_PREFAB, onAddPrefab) +} diff --git a/opendc-web/opendc-web-ui/src/sagas/objects.js b/opendc-web/opendc-web-ui/src/sagas/objects.js new file mode 100644 index 00000000..313d9976 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/sagas/objects.js @@ -0,0 +1,229 @@ +import { call, put, select } from 'redux-saga/effects' +import { addToStore } from '../actions/objects' +import { getAllSchedulers } from '../api/routes/schedulers' +import { getProject } from '../api/routes/projects' +import { getAllTraces } from '../api/routes/traces' +import { getUser } from '../api/routes/users' +import { getTopology, updateTopology } from '../api/routes/topologies' +import { uuid } from 'uuidv4' + +export const OBJECT_SELECTORS = { + project: (state) => state.objects.project, + user: (state) => state.objects.user, + authorization: (state) => state.objects.authorization, + portfolio: (state) => state.objects.portfolio, + scenario: (state) => state.objects.scenario, + cpu: (state) => state.objects.cpu, + gpu: (state) => state.objects.gpu, + memory: (state) => state.objects.memory, + storage: (state) => state.objects.storage, + machine: (state) => state.objects.machine, + rack: (state) => state.objects.rack, + tile: (state) => state.objects.tile, + room: (state) => state.objects.room, + topology: (state) => state.objects.topology, +} + +function* fetchAndStoreObject(objectType, id, apiCall) { + const objectStore = yield select(OBJECT_SELECTORS[objectType]) + let object = objectStore[id] + if (!object) { + object = yield apiCall + yield put(addToStore(objectType, object)) + } + return object +} + +function* fetchAndStoreObjects(objectType, apiCall) { + const objects = yield apiCall + for (let object of objects) { + yield put(addToStore(objectType, object)) + } + return objects +} + +export const fetchAndStoreProject = (id) => fetchAndStoreObject('project', id, call(getProject, id)) + +export const fetchAndStoreUser = (id) => fetchAndStoreObject('user', id, call(getUser, id)) + +export const fetchAndStoreTopology = function* (id) { + const topologyStore = yield select(OBJECT_SELECTORS['topology']) + const roomStore = yield select(OBJECT_SELECTORS['room']) + const tileStore = yield select(OBJECT_SELECTORS['tile']) + const rackStore = yield select(OBJECT_SELECTORS['rack']) + const machineStore = yield select(OBJECT_SELECTORS['machine']) + + let topology = topologyStore[id] + if (!topology) { + const fullTopology = yield call(getTopology, id) + + for (let roomIdx in fullTopology.rooms) { + const fullRoom = fullTopology.rooms[roomIdx] + + generateIdIfNotPresent(fullRoom) + + if (!roomStore[fullRoom._id]) { + for (let tileIdx in fullRoom.tiles) { + const fullTile = fullRoom.tiles[tileIdx] + + generateIdIfNotPresent(fullTile) + + if (!tileStore[fullTile._id]) { + if (fullTile.rack) { + const fullRack = fullTile.rack + + generateIdIfNotPresent(fullRack) + + if (!rackStore[fullRack._id]) { + for (let machineIdx in fullRack.machines) { + const fullMachine = fullRack.machines[machineIdx] + + generateIdIfNotPresent(fullMachine) + + if (!machineStore[fullMachine._id]) { + let machine = (({ _id, position, cpus, gpus, memories, storages }) => ({ + _id, + rackId: fullRack._id, + position, + cpuIds: cpus.map((u) => u._id), + gpuIds: gpus.map((u) => u._id), + memoryIds: memories.map((u) => u._id), + storageIds: storages.map((u) => u._id), + }))(fullMachine) + yield put(addToStore('machine', machine)) + } + } + + const filledSlots = new Array(fullRack.capacity).fill(null) + fullRack.machines.forEach( + (machine) => (filledSlots[machine.position - 1] = machine._id) + ) + let rack = (({ _id, name, capacity, powerCapacityW }) => ({ + _id, + name, + capacity, + powerCapacityW, + machineIds: filledSlots, + }))(fullRack) + yield put(addToStore('rack', rack)) + } + } + + let tile = (({ _id, positionX, positionY, rack }) => ({ + _id, + roomId: fullRoom._id, + positionX, + positionY, + rackId: rack ? rack._id : undefined, + }))(fullTile) + yield put(addToStore('tile', tile)) + } + } + + let room = (({ _id, name, tiles }) => ({ _id, name, tileIds: tiles.map((t) => t._id) }))(fullRoom) + yield put(addToStore('room', room)) + } + } + + topology = (({ _id, name, rooms }) => ({ _id, name, roomIds: rooms.map((r) => r._id) }))(fullTopology) + yield put(addToStore('topology', topology)) + + // TODO consider pushing the IDs + } + + return topology +} + +const generateIdIfNotPresent = (obj) => { + if (!obj._id) { + obj._id = uuid() + } +} + +export const updateTopologyOnServer = function* (id) { + const topology = yield getTopologyAsObject(id, true) + + yield call(updateTopology, topology) +} + +export const getTopologyAsObject = function* (id, keepIds) { + const topologyStore = yield select(OBJECT_SELECTORS['topology']) + const rooms = yield getAllRooms(topologyStore[id].roomIds, keepIds) + return { + _id: keepIds ? id : undefined, + name: topologyStore[id].name, + rooms: rooms, + } +} + +export const getAllRooms = function* (roomIds, keepIds) { + const roomStore = yield select(OBJECT_SELECTORS['room']) + + let rooms = [] + + for (let id of roomIds) { + let tiles = yield getAllRoomTiles(roomStore[id], keepIds) + rooms.push({ + _id: keepIds ? id : undefined, + name: roomStore[id].name, + tiles: tiles, + }) + } + return rooms +} + +export const getAllRoomTiles = function* (roomStore, keepIds) { + let tiles = [] + + for (let id of roomStore.tileIds) { + tiles.push(yield getTileById(id, keepIds)) + } + return tiles +} + +export const getTileById = function* (id, keepIds) { + const tileStore = yield select(OBJECT_SELECTORS['tile']) + return { + _id: keepIds ? id : undefined, + positionX: tileStore[id].positionX, + positionY: tileStore[id].positionY, + rack: !tileStore[id].rackId ? undefined : yield getRackById(tileStore[id].rackId, keepIds), + } +} + +export const getRackById = function* (id, keepIds) { + const rackStore = yield select(OBJECT_SELECTORS['rack']) + const machineStore = yield select(OBJECT_SELECTORS['machine']) + const cpuStore = yield select(OBJECT_SELECTORS['cpu']) + const gpuStore = yield select(OBJECT_SELECTORS['gpu']) + const memoryStore = yield select(OBJECT_SELECTORS['memory']) + const storageStore = yield select(OBJECT_SELECTORS['storage']) + + return { + _id: keepIds ? rackStore[id]._id : undefined, + name: rackStore[id].name, + capacity: rackStore[id].capacity, + powerCapacityW: rackStore[id].powerCapacityW, + machines: rackStore[id].machineIds + .filter((m) => m !== null) + .map((machineId) => ({ + _id: keepIds ? machineId : undefined, + position: machineStore[machineId].position, + cpus: machineStore[machineId].cpuIds.map((id) => cpuStore[id]), + gpus: machineStore[machineId].gpuIds.map((id) => gpuStore[id]), + memories: machineStore[machineId].memoryIds.map((id) => memoryStore[id]), + storages: machineStore[machineId].storageIds.map((id) => storageStore[id]), + })), + } +} + +export const fetchAndStoreAllTraces = () => fetchAndStoreObjects('trace', call(getAllTraces)) + +export const fetchAndStoreAllSchedulers = function* () { + const objects = yield call(getAllSchedulers) + for (let object of objects) { + object._id = object.name + yield put(addToStore('scheduler', object)) + } + return objects +} diff --git a/opendc-web/opendc-web-ui/src/sagas/portfolios.js b/opendc-web/opendc-web-ui/src/sagas/portfolios.js new file mode 100644 index 00000000..ed9bfd29 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/sagas/portfolios.js @@ -0,0 +1,131 @@ +import { call, put, select, delay } from 'redux-saga/effects' +import { addPropToStoreObject, addToStore } from '../actions/objects' +import { addPortfolio, deletePortfolio, getPortfolio, updatePortfolio } from '../api/routes/portfolios' +import { getProject } from '../api/routes/projects' +import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' +import { fetchAndStoreAllTopologiesOfProject } from './topology' +import { getScenario } from '../api/routes/scenarios' + +export function* onOpenPortfolioSucceeded(action) { + try { + const project = yield call(getProject, action.projectId) + yield put(addToStore('project', project)) + yield fetchAndStoreAllTopologiesOfProject(project._id) + yield fetchPortfoliosOfProject() + yield fetchAndStoreAllSchedulers() + yield fetchAndStoreAllTraces() + + yield watchForPortfolioResults() + } catch (error) { + console.error(error) + } +} + +export function* watchForPortfolioResults() { + try { + const currentPortfolioId = yield select((state) => state.currentPortfolioId) + let unfinishedScenarios = yield getCurrentUnfinishedScenarios() + + while (unfinishedScenarios.length > 0) { + yield delay(3000) + yield fetchPortfolioWithScenarios(currentPortfolioId) + unfinishedScenarios = yield getCurrentUnfinishedScenarios() + } + } catch (error) { + console.error(error) + } +} + +export function* getCurrentUnfinishedScenarios() { + try { + const currentPortfolioId = yield select((state) => state.currentPortfolioId) + const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds) + const scenarioObjects = yield select((state) => state.objects.scenario) + const scenarios = scenarioIds.map((s) => scenarioObjects[s]) + return scenarios.filter((s) => !s || s.simulation.state === 'QUEUED' || s.simulation.state === 'RUNNING') + } catch (error) { + console.error(error) + } +} + +export function* fetchPortfoliosOfProject() { + try { + const currentProjectId = yield select((state) => state.currentProjectId) + const currentProject = yield select((state) => state.objects.project[currentProjectId]) + + yield fetchAndStoreAllSchedulers() + yield fetchAndStoreAllTraces() + + for (let i in currentProject.portfolioIds) { + yield fetchPortfolioWithScenarios(currentProject.portfolioIds[i]) + } + } catch (error) { + console.error(error) + } +} + +export function* fetchPortfolioWithScenarios(portfolioId) { + try { + const portfolio = yield call(getPortfolio, portfolioId) + yield put(addToStore('portfolio', portfolio)) + + for (let i in portfolio.scenarioIds) { + const scenario = yield call(getScenario, portfolio.scenarioIds[i]) + yield put(addToStore('scenario', scenario)) + } + return portfolio + } catch (error) { + console.error(error) + } +} + +export function* onAddPortfolio(action) { + try { + const currentProjectId = yield select((state) => state.currentProjectId) + + const portfolio = yield call( + addPortfolio, + currentProjectId, + Object.assign({}, action.portfolio, { + projectId: currentProjectId, + scenarioIds: [], + }) + ) + yield put(addToStore('portfolio', portfolio)) + + const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds) + yield put( + addPropToStoreObject('project', currentProjectId, { + portfolioIds: portfolioIds.concat([portfolio._id]), + }) + ) + } catch (error) { + console.error(error) + } +} + +export function* onUpdatePortfolio(action) { + try { + const portfolio = yield call(updatePortfolio, action.portfolio._id, action.portfolio) + yield put(addToStore('portfolio', portfolio)) + } catch (error) { + console.error(error) + } +} + +export function* onDeletePortfolio(action) { + try { + yield call(deletePortfolio, action.id) + + const currentProjectId = yield select((state) => state.currentProjectId) + const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds) + + yield put( + addPropToStoreObject('project', currentProjectId, { + portfolioIds: portfolioIds.filter((id) => id !== action.id), + }) + ) + } catch (error) { + console.error(error) + } +} diff --git a/opendc-web/opendc-web-ui/src/sagas/prefabs.js b/opendc-web/opendc-web-ui/src/sagas/prefabs.js new file mode 100644 index 00000000..16cf3d62 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/sagas/prefabs.js @@ -0,0 +1,15 @@ +import { call, put, select } from 'redux-saga/effects' +import { addToStore } from '../actions/objects' +import { addPrefab } from '../api/routes/prefabs' +import { getRackById } from './objects' + +export function* onAddPrefab(action) { + try { + const currentRackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId) + const currentRackJson = yield getRackById(currentRackId, false) + const prefab = yield call(addPrefab, { name: action.name, rack: currentRackJson }) + yield put(addToStore('prefab', prefab)) + } catch (error) { + console.error(error) + } +} diff --git a/opendc-web/opendc-web-ui/src/sagas/profile.js b/opendc-web/opendc-web-ui/src/sagas/profile.js new file mode 100644 index 00000000..e914ba56 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/sagas/profile.js @@ -0,0 +1,12 @@ +import { call, put } from 'redux-saga/effects' +import { deleteCurrentUserSucceeded } from '../actions/users' +import { deleteUser } from '../api/routes/users' + +export function* onDeleteCurrentUser(action) { + try { + yield call(deleteUser, action.userId) + yield put(deleteCurrentUserSucceeded()) + } catch (error) { + console.error(error) + } +} diff --git a/opendc-web/opendc-web-ui/src/sagas/projects.js b/opendc-web/opendc-web-ui/src/sagas/projects.js new file mode 100644 index 00000000..fdeea132 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/sagas/projects.js @@ -0,0 +1,48 @@ +import { call, put } from 'redux-saga/effects' +import { addToStore } from '../actions/objects' +import { addProjectSucceeded, deleteProjectSucceeded } from '../actions/projects' +import { addProject, deleteProject, getProject } from '../api/routes/projects' +import { fetchAndStoreAllTopologiesOfProject } from './topology' +import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' +import { fetchPortfoliosOfProject } from './portfolios' + +export function* onOpenProjectSucceeded(action) { + try { + const project = yield call(getProject, action.id) + yield put(addToStore('project', project)) + + yield fetchAndStoreAllTopologiesOfProject(action.id, true) + yield fetchPortfoliosOfProject() + yield fetchAndStoreAllSchedulers() + yield fetchAndStoreAllTraces() + } catch (error) { + console.error(error) + } +} + +export function* onProjectAdd(action) { + try { + const project = yield call(addProject, { name: action.name }) + yield put(addToStore('project', project)) + + const authorization = { + projectId: project._id, + userId: action.userId, + authorizationLevel: 'OWN', + project, + } + yield put(addToStore('authorization', authorization)) + yield put(addProjectSucceeded([authorization.userId, authorization.projectId])) + } catch (error) { + console.error(error) + } +} + +export function* onProjectDelete(action) { + try { + yield call(deleteProject, action.id) + yield put(deleteProjectSucceeded(action.id)) + } catch (error) { + console.error(error) + } +} diff --git a/opendc-web/opendc-web-ui/src/sagas/scenarios.js b/opendc-web/opendc-web-ui/src/sagas/scenarios.js new file mode 100644 index 00000000..59223610 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/sagas/scenarios.js @@ -0,0 +1,65 @@ +import { call, put, select } from 'redux-saga/effects' +import { addPropToStoreObject, addToStore } from '../actions/objects' +import { getProject } from '../api/routes/projects' +import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' +import { fetchAndStoreAllTopologiesOfProject } from './topology' +import { addScenario, deleteScenario, updateScenario } from '../api/routes/scenarios' +import { fetchPortfolioWithScenarios, watchForPortfolioResults } from './portfolios' + +export function* onOpenScenarioSucceeded(action) { + try { + const project = yield call(getProject, action.projectId) + yield put(addToStore('project', project)) + yield fetchAndStoreAllTopologiesOfProject(project._id) + yield fetchAndStoreAllSchedulers() + yield fetchAndStoreAllTraces() + yield fetchPortfolioWithScenarios(action.portfolioId) + + // TODO Fetch scenario-specific metrics + } catch (error) { + console.error(error) + } +} + +export function* onAddScenario(action) { + try { + const scenario = yield call(addScenario, action.scenario.portfolioId, action.scenario) + yield put(addToStore('scenario', scenario)) + + const scenarioIds = yield select((state) => state.objects.portfolio[action.scenario.portfolioId].scenarioIds) + yield put( + addPropToStoreObject('portfolio', action.scenario.portfolioId, { + scenarioIds: scenarioIds.concat([scenario._id]), + }) + ) + yield watchForPortfolioResults() + } catch (error) { + console.error(error) + } +} + +export function* onUpdateScenario(action) { + try { + const scenario = yield call(updateScenario, action.scenario._id, action.scenario) + yield put(addToStore('scenario', scenario)) + } catch (error) { + console.error(error) + } +} + +export function* onDeleteScenario(action) { + try { + yield call(deleteScenario, action.id) + + const currentPortfolioId = yield select((state) => state.currentPortfolioId) + const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds) + + yield put( + addPropToStoreObject('scenario', currentPortfolioId, { + scenarioIds: scenarioIds.filter((id) => id !== action.id), + }) + ) + } catch (error) { + console.error(error) + } +} diff --git a/opendc-web/opendc-web-ui/src/sagas/topology.js b/opendc-web/opendc-web-ui/src/sagas/topology.js new file mode 100644 index 00000000..bba1ebb1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/sagas/topology.js @@ -0,0 +1,311 @@ +import { call, put, select } from 'redux-saga/effects' +import { goDownOneInteractionLevel } from '../actions/interaction-level' +import { + addIdToStoreObjectListProp, + addPropToStoreObject, + addToStore, + removeIdFromStoreObjectListProp, +} from '../actions/objects' +import { + cancelNewRoomConstructionSucceeded, + setCurrentTopology, + startNewRoomConstructionSucceeded, +} from '../actions/topology/building' +import { + DEFAULT_RACK_POWER_CAPACITY, + DEFAULT_RACK_SLOT_CAPACITY, + MAX_NUM_UNITS_PER_MACHINE, +} from '../components/app/map/MapConstants' +import { fetchAndStoreTopology, getTopologyAsObject, updateTopologyOnServer } from './objects' +import { uuid } from 'uuidv4' +import { addTopology, deleteTopology } from '../api/routes/topologies' + +export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = false) { + try { + const project = yield select((state) => state.objects.project[projectId]) + + for (let i in project.topologyIds) { + yield fetchAndStoreTopology(project.topologyIds[i]) + } + + if (setTopology) { + yield put(setCurrentTopology(project.topologyIds[0])) + } + } catch (error) { + console.error(error) + } +} + +export function* onAddTopology(action) { + try { + const currentProjectId = yield select((state) => state.currentProjectId) + + let topologyToBeCreated + if (action.duplicateId) { + topologyToBeCreated = yield getTopologyAsObject(action.duplicateId, false) + topologyToBeCreated = Object.assign({}, topologyToBeCreated, { + name: action.name, + }) + } else { + topologyToBeCreated = { name: action.name, rooms: [] } + } + + const topology = yield call( + addTopology, + Object.assign({}, topologyToBeCreated, { + projectId: currentProjectId, + }) + ) + yield fetchAndStoreTopology(topology._id) + + const topologyIds = yield select((state) => state.objects.project[currentProjectId].topologyIds) + yield put( + addPropToStoreObject('project', currentProjectId, { + topologyIds: topologyIds.concat([topology._id]), + }) + ) + yield put(setCurrentTopology(topology._id)) + } catch (error) { + console.error(error) + } +} + +export function* onDeleteTopology(action) { + try { + const currentProjectId = yield select((state) => state.currentProjectId) + const topologyIds = yield select((state) => state.objects.project[currentProjectId].topologyIds) + const currentTopologyId = yield select((state) => state.currentTopologyId) + if (currentTopologyId === action.id) { + yield put(setCurrentTopology(topologyIds.filter((t) => t !== action.id)[0])) + } + + yield call(deleteTopology, action.id) + + yield put( + addPropToStoreObject('project', currentProjectId, { + topologyIds: topologyIds.filter((id) => id !== action.id), + }) + ) + } catch (error) { + console.error(error) + } +} + +export function* onStartNewRoomConstruction() { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const room = { + _id: uuid(), + name: 'Room', + topologyId, + tileIds: [], + } + yield put(addToStore('room', room)) + yield put(addIdToStoreObjectListProp('topology', topologyId, 'roomIds', room._id)) + yield updateTopologyOnServer(topologyId) + yield put(startNewRoomConstructionSucceeded(room._id)) + } catch (error) { + console.error(error) + } +} + +export function* onCancelNewRoomConstruction() { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const roomId = yield select((state) => state.construction.currentRoomInConstruction) + yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'roomIds', roomId)) + // TODO remove room from store, too + yield updateTopologyOnServer(topologyId) + yield put(cancelNewRoomConstructionSucceeded()) + } catch (error) { + console.error(error) + } +} + +export function* onAddTile(action) { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const roomId = yield select((state) => state.construction.currentRoomInConstruction) + const tile = { + _id: uuid(), + roomId, + positionX: action.positionX, + positionY: action.positionY, + } + yield put(addToStore('tile', tile)) + yield put(addIdToStoreObjectListProp('room', roomId, 'tileIds', tile._id)) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} + +export function* onDeleteTile(action) { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const roomId = yield select((state) => state.construction.currentRoomInConstruction) + yield put(removeIdFromStoreObjectListProp('room', roomId, 'tileIds', action.tileId)) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} + +export function* onEditRoomName(action) { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const roomId = yield select((state) => state.interactionLevel.roomId) + const room = Object.assign({}, yield select((state) => state.objects.room[roomId])) + room.name = action.name + yield put(addPropToStoreObject('room', roomId, { name: action.name })) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} + +export function* onDeleteRoom() { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const roomId = yield select((state) => state.interactionLevel.roomId) + yield put(goDownOneInteractionLevel()) + yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'roomIds', roomId)) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} + +export function* onEditRackName(action) { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId) + const rack = Object.assign({}, yield select((state) => state.objects.rack[rackId])) + rack.name = action.name + yield put(addPropToStoreObject('rack', rackId, { name: action.name })) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} + +export function* onDeleteRack() { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const tileId = yield select((state) => state.interactionLevel.tileId) + yield put(goDownOneInteractionLevel()) + yield put(addPropToStoreObject('tile', tileId, { rackId: undefined })) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} + +export function* onAddRackToTile(action) { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const rack = { + _id: uuid(), + name: 'Rack', + capacity: DEFAULT_RACK_SLOT_CAPACITY, + powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, + } + rack.machineIds = new Array(rack.capacity).fill(null) + yield put(addToStore('rack', rack)) + yield put(addPropToStoreObject('tile', action.tileId, { rackId: rack._id })) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} + +export function* onAddMachine(action) { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const rackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId) + const rack = yield select((state) => state.objects.rack[rackId]) + + const machine = { + _id: uuid(), + rackId, + position: action.position, + cpuIds: [], + gpuIds: [], + memoryIds: [], + storageIds: [], + } + yield put(addToStore('machine', machine)) + + const machineIds = [...rack.machineIds] + machineIds[machine.position - 1] = machine._id + yield put(addPropToStoreObject('rack', rackId, { machineIds })) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} + +export function* onDeleteMachine() { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const tileId = yield select((state) => state.interactionLevel.tileId) + const position = yield select((state) => state.interactionLevel.position) + const rack = yield select((state) => state.objects.rack[state.objects.tile[tileId].rackId]) + const machineIds = [...rack.machineIds] + machineIds[position - 1] = null + yield put(goDownOneInteractionLevel()) + yield put(addPropToStoreObject('rack', rack._id, { machineIds })) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} + +export function* onAddUnit(action) { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const tileId = yield select((state) => state.interactionLevel.tileId) + const position = yield select((state) => state.interactionLevel.position) + const machine = yield select( + (state) => + state.objects.machine[state.objects.rack[state.objects.tile[tileId].rackId].machineIds[position - 1]] + ) + + if (machine[action.unitType + 'Ids'].length >= MAX_NUM_UNITS_PER_MACHINE) { + return + } + + const units = [...machine[action.unitType + 'Ids'], action.id] + yield put( + addPropToStoreObject('machine', machine._id, { + [action.unitType + 'Ids']: units, + }) + ) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} + +export function* onDeleteUnit(action) { + try { + const topologyId = yield select((state) => state.currentTopologyId) + const tileId = yield select((state) => state.interactionLevel.tileId) + const position = yield select((state) => state.interactionLevel.position) + const machine = yield select( + (state) => + state.objects.machine[state.objects.rack[state.objects.tile[tileId].rackId].machineIds[position - 1]] + ) + const unitIds = machine[action.unitType + 'Ids'].slice() + unitIds.splice(action.index, 1) + + yield put( + addPropToStoreObject('machine', machine._id, { + [action.unitType + 'Ids']: unitIds, + }) + ) + yield updateTopologyOnServer(topologyId) + } catch (error) { + console.error(error) + } +} diff --git a/opendc-web/opendc-web-ui/src/sagas/users.js b/opendc-web/opendc-web-ui/src/sagas/users.js new file mode 100644 index 00000000..74e652f6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/sagas/users.js @@ -0,0 +1,44 @@ +import { call, put } from 'redux-saga/effects' +import { logInSucceeded } from '../actions/auth' +import { addToStore } from '../actions/objects' +import { fetchAuthorizationsOfCurrentUserSucceeded } from '../actions/users' +import { performTokenSignIn } from '../api/routes/token-signin' +import { addUser } from '../api/routes/users' +import { saveAuthLocalStorage } from '../auth/index' +import { fetchAndStoreProject, fetchAndStoreUser } from './objects' + +export function* onFetchLoggedInUser(action) { + try { + const tokenResponse = yield call(performTokenSignIn, action.payload.authToken) + + let userId = tokenResponse.userId + + if (tokenResponse.isNewUser) { + saveAuthLocalStorage({ authToken: action.payload.authToken }) + const newUser = yield call(addUser, action.payload) + userId = newUser._id + } + + yield put(logInSucceeded(Object.assign({ userId }, action.payload))) + } catch (error) { + console.error(error) + } +} + +export function* onFetchAuthorizationsOfCurrentUser(action) { + try { + const user = yield call(fetchAndStoreUser, action.userId) + + for (const authorization of user.authorizations) { + authorization.userId = action.userId + yield put(addToStore('authorization', authorization)) + yield fetchAndStoreProject(authorization.projectId) + } + + const authorizationIds = user.authorizations.map((authorization) => [action.userId, authorization.projectId]) + + yield put(fetchAuthorizationsOfCurrentUserSucceeded(authorizationIds)) + } catch (error) { + console.error(error) + } +} diff --git a/opendc-web/opendc-web-ui/src/shapes/index.js b/opendc-web/opendc-web-ui/src/shapes/index.js new file mode 100644 index 00000000..9fab6f5d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/shapes/index.js @@ -0,0 +1,148 @@ +import PropTypes from 'prop-types' + +const Shapes = {} + +Shapes.User = PropTypes.shape({ + _id: PropTypes.string.isRequired, + googleId: PropTypes.string.isRequired, + email: PropTypes.string.isRequired, + givenName: PropTypes.string.isRequired, + familyName: PropTypes.string.isRequired, + authorizations: PropTypes.array.isRequired, +}) + +Shapes.Project = PropTypes.shape({ + _id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + datetimeCreated: PropTypes.string.isRequired, + datetimeLastEdited: PropTypes.string.isRequired, + topologyIds: PropTypes.array.isRequired, + portfolioIds: PropTypes.array.isRequired, +}) + +Shapes.Authorization = PropTypes.shape({ + userId: PropTypes.string.isRequired, + user: Shapes.User, + projectId: PropTypes.string.isRequired, + project: Shapes.Project, + authorizationLevel: PropTypes.string.isRequired, +}) + +Shapes.ProcessingUnit = PropTypes.shape({ + _id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + clockRateMhz: PropTypes.number.isRequired, + numberOfCores: PropTypes.number.isRequired, + energyConsumptionW: PropTypes.number.isRequired, +}) + +Shapes.StorageUnit = PropTypes.shape({ + _id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + speedMbPerS: PropTypes.number.isRequired, + sizeMb: PropTypes.number.isRequired, + energyConsumptionW: PropTypes.number.isRequired, +}) + +Shapes.Machine = PropTypes.shape({ + _id: PropTypes.string.isRequired, + rackId: PropTypes.string.isRequired, + position: PropTypes.number.isRequired, + cpuIds: PropTypes.arrayOf(PropTypes.string.isRequired), + cpus: PropTypes.arrayOf(Shapes.ProcessingUnit), + gpuIds: PropTypes.arrayOf(PropTypes.string.isRequired), + gpus: PropTypes.arrayOf(Shapes.ProcessingUnit), + memoryIds: PropTypes.arrayOf(PropTypes.string.isRequired), + memories: PropTypes.arrayOf(Shapes.StorageUnit), + storageIds: PropTypes.arrayOf(PropTypes.string.isRequired), + storages: PropTypes.arrayOf(Shapes.StorageUnit), +}) + +Shapes.Rack = PropTypes.shape({ + _id: PropTypes.string.isRequired, + capacity: PropTypes.number.isRequired, + powerCapacityW: PropTypes.number.isRequired, + machines: PropTypes.arrayOf(Shapes.Machine), +}) + +Shapes.Tile = PropTypes.shape({ + _id: PropTypes.string.isRequired, + roomId: PropTypes.string.isRequired, + positionX: PropTypes.number.isRequired, + positionY: PropTypes.number.isRequired, + rackId: PropTypes.string, + rack: Shapes.Rack, +}) + +Shapes.Room = PropTypes.shape({ + _id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + tiles: PropTypes.arrayOf(Shapes.Tile), +}) + +Shapes.Topology = PropTypes.shape({ + _id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + rooms: PropTypes.arrayOf(Shapes.Room), +}) + +Shapes.Scheduler = PropTypes.shape({ + name: PropTypes.string.isRequired, +}) + +Shapes.Trace = PropTypes.shape({ + _id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, +}) + +Shapes.Portfolio = PropTypes.shape({ + _id: PropTypes.string.isRequired, + projectId: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + scenarioIds: PropTypes.arrayOf(PropTypes.string).isRequired, + targets: PropTypes.shape({ + enabledMetrics: PropTypes.arrayOf(PropTypes.string).isRequired, + repeatsPerScenario: PropTypes.number.isRequired, + }).isRequired, +}) + +Shapes.Scenario = PropTypes.shape({ + _id: PropTypes.string.isRequired, + portfolioId: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + simulation: PropTypes.shape({ + state: PropTypes.string.isRequired, + }).isRequired, + trace: PropTypes.shape({ + traceId: PropTypes.string.isRequired, + trace: Shapes.Trace, + loadSamplingFraction: PropTypes.number.isRequired, + }).isRequired, + topology: PropTypes.shape({ + topologyId: PropTypes.string.isRequired, + topology: Shapes.Topology, + }).isRequired, + operational: PropTypes.shape({ + failuresEnabled: PropTypes.bool.isRequired, + performanceInterferenceEnabled: PropTypes.bool.isRequired, + schedulerName: PropTypes.string.isRequired, + scheduler: Shapes.Scheduler, + }).isRequired, + results: PropTypes.object, +}) + +Shapes.WallSegment = PropTypes.shape({ + startPosX: PropTypes.number.isRequired, + startPosY: PropTypes.number.isRequired, + isHorizontal: PropTypes.bool.isRequired, + length: PropTypes.number.isRequired, +}) + +Shapes.InteractionLevel = PropTypes.shape({ + mode: PropTypes.string.isRequired, + roomId: PropTypes.string, + rackId: PropTypes.string, +}) + +export default Shapes diff --git a/opendc-web/opendc-web-ui/src/shortcuts/keymap.js b/opendc-web/opendc-web-ui/src/shortcuts/keymap.js new file mode 100644 index 00000000..797340d7 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/shortcuts/keymap.js @@ -0,0 +1,10 @@ +const KeymapConfiguration = { + MAP: { + MOVE_LEFT: ['a', 'left'], + MOVE_RIGHT: ['d', 'right'], + MOVE_UP: ['w', 'up'], + MOVE_DOWN: ['s', 'down'], + }, +} + +export default KeymapConfiguration diff --git a/opendc-web/opendc-web-ui/src/store/configure-store.js b/opendc-web/opendc-web-ui/src/store/configure-store.js new file mode 100644 index 00000000..d8f343ed --- /dev/null +++ b/opendc-web/opendc-web-ui/src/store/configure-store.js @@ -0,0 +1,35 @@ +import { applyMiddleware, compose, createStore } from 'redux' +import persistState from 'redux-localstorage' +import { createLogger } from 'redux-logger' +import createSagaMiddleware from 'redux-saga' +import thunk from 'redux-thunk' +import { authRedirectMiddleware } from '../auth/index' +import rootReducer from '../reducers/index' +import rootSaga from '../sagas/index' +import { dummyMiddleware } from './middlewares/dummy-middleware' +import { viewportAdjustmentMiddleware } from './middlewares/viewport-adjustment' + +const sagaMiddleware = createSagaMiddleware() + +let logger +if (process.env.NODE_ENV !== 'production') { + logger = createLogger() +} + +const middlewares = [ + process.env.NODE_ENV === 'production' ? dummyMiddleware : logger, + thunk, + sagaMiddleware, + authRedirectMiddleware, + viewportAdjustmentMiddleware, +] + +export let store = undefined + +export default function configureStore() { + const configuredStore = createStore(rootReducer, compose(persistState('auth'), applyMiddleware(...middlewares))) + sagaMiddleware.run(rootSaga) + store = configuredStore + + return configuredStore +} diff --git a/opendc-web/opendc-web-ui/src/store/middlewares/dummy-middleware.js b/opendc-web/opendc-web-ui/src/store/middlewares/dummy-middleware.js new file mode 100644 index 00000000..5ba35691 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/store/middlewares/dummy-middleware.js @@ -0,0 +1,3 @@ +export const dummyMiddleware = (store) => (next) => (action) => { + next(action) +} diff --git a/opendc-web/opendc-web-ui/src/store/middlewares/viewport-adjustment.js b/opendc-web/opendc-web-ui/src/store/middlewares/viewport-adjustment.js new file mode 100644 index 00000000..b4472c54 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/store/middlewares/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/style-globals/_mixins.sass b/opendc-web/opendc-web-ui/src/style-globals/_mixins.sass new file mode 100644 index 00000000..d0a8d1ac --- /dev/null +++ b/opendc-web/opendc-web-ui/src/style-globals/_mixins.sass @@ -0,0 +1,21 @@ +=transition($property, $time) + -webkit-transition: $property $time + -moz-transition: $property $time + -o-transition: $property $time + transition: $property $time + +=user-select + -webkit-user-select: none + -moz-user-select: none + -ms-user-select: none + user-select: none + +=border-radius($length) + -webkit-border-radius: $length + -moz-border-radius: $length + border-radius: $length + +/* General Button Abstractions */ +=clickable + cursor: pointer + +user-select diff --git a/opendc-web/opendc-web-ui/src/style-globals/_variables.sass b/opendc-web/opendc-web-ui/src/style-globals/_variables.sass new file mode 100644 index 00000000..7553caa0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/style-globals/_variables.sass @@ -0,0 +1,31 @@ +// Sizes and Margins +$document-padding: 20px +$inter-element-margin: 5px +$standard-border-radius: 5px +$side-menu-width: 350px +$color-indicator-width: 140px + +$global-padding: 30px +$side-bar-width: 350px +$navbar-height: 50px +$navbar-padding: 10px + +// Durations +$transition-length: 150ms + +// Colors +$gray-very-dark: #5c5c5c +$gray-dark: #aaa +$gray-semi-dark: #bbb +$gray-semi-light: #ccc +$gray-light: #ddd +$gray-very-light: #eee +$blue: #00A6D6 +$blue-dark: #0087b5 +$blue-very-dark: #006182 +$blue-light: #deebf7 + +// Media queries +$screen-sm: 768px +$screen-md: 992px +$screen-lg: 1200px diff --git a/opendc-web/opendc-web-ui/src/util/authorizations.js b/opendc-web/opendc-web-ui/src/util/authorizations.js new file mode 100644 index 00000000..4086b35d --- /dev/null +++ b/opendc-web/opendc-web-ui/src/util/authorizations.js @@ -0,0 +1,11 @@ +export const AUTH_ICON_MAP = { + OWN: 'home', + EDIT: 'pencil', + VIEW: 'eye', +} + +export const AUTH_DESCRIPTION_MAP = { + OWN: 'Own', + EDIT: 'Can Edit', + VIEW: 'Can View', +} diff --git a/opendc-web/opendc-web-ui/src/util/available-metrics.js b/opendc-web/opendc-web-ui/src/util/available-metrics.js new file mode 100644 index 00000000..807bc0c1 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/util/available-metrics.js @@ -0,0 +1,67 @@ +export const AVAILABLE_METRICS = [ + 'total_overcommitted_burst', + 'total_power_draw', + 'total_failure_vm_slices', + 'total_granted_burst', + 'total_interfered_burst', + 'total_requested_burst', + 'mean_cpu_usage', + 'mean_cpu_demand', + 'mean_num_deployed_images', + 'max_num_deployed_images', + 'total_vms_submitted', + 'total_vms_queued', + 'total_vms_finished', + 'total_vms_failed', +] + +export const METRIC_NAMES_SHORT = { + total_overcommitted_burst: 'Overcomm. CPU Cycles', + total_granted_burst: 'Granted CPU Cycles', + total_requested_burst: 'Requested CPU Cycles', + total_interfered_burst: 'Interfered CPU Cycles', + total_power_draw: 'Total Power Consumption', + mean_cpu_usage: 'Mean Host CPU Usage', + mean_cpu_demand: 'Mean Host CPU Demand', + mean_num_deployed_images: 'Mean Num. Deployed Images Per Host', + max_num_deployed_images: 'Max. Num. Deployed Images Per Host', + total_failure_vm_slices: 'Total Num. Failed VM Slices', + total_vms_submitted: 'Total Num. VMs Submitted', + total_vms_queued: 'Max. Num. VMs Queued', + total_vms_finished: 'Max. Num. VMs Finished', + total_vms_failed: 'Max. Num. VMs Failed', +} + +export const METRIC_NAMES = { + total_overcommitted_burst: 'Overcommitted CPU Cycles', + total_granted_burst: 'Granted CPU Cycles', + total_requested_burst: 'Requested CPU Cycles', + total_interfered_burst: 'Interfered CPU Cycles', + total_power_draw: 'Total Power Consumption', + mean_cpu_usage: 'Mean Host CPU Usage', + mean_cpu_demand: 'Mean Host CPU Demand', + mean_num_deployed_images: 'Mean Number of Deployed Images Per Host', + max_num_deployed_images: 'Maximum Number Deployed Images Per Host', + total_failure_vm_slices: 'Total Number Failed VM Slices', + total_vms_submitted: 'Total Number VMs Submitted', + total_vms_queued: 'Maximum Number VMs Queued', + total_vms_finished: 'Maximum Number VMs Finished', + total_vms_failed: 'Maximum Number VMs Failed', +} + +export const METRIC_UNITS = { + total_overcommitted_burst: 'MFLOP', + total_granted_burst: 'MFLOP', + total_requested_burst: 'MFLOP', + total_interfered_burst: 'MFLOP', + total_power_draw: 'Wh', + mean_cpu_usage: 'MHz', + mean_cpu_demand: 'MHz', + mean_num_deployed_images: 'VMs', + max_num_deployed_images: 'VMs', + total_failure_vm_slices: 'VM Slices', + total_vms_submitted: 'VMs', + total_vms_queued: 'VMs', + total_vms_finished: 'VMs', + total_vms_failed: 'VMs', +} diff --git a/opendc-web/opendc-web-ui/src/util/colors.js b/opendc-web/opendc-web-ui/src/util/colors.js new file mode 100644 index 00000000..34468503 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/util/colors.js @@ -0,0 +1,29 @@ +export const GRID_COLOR = 'rgba(0, 0, 0, 0.5)' +export const BACKDROP_COLOR = 'rgba(255, 255, 255, 1)' +export const WALL_COLOR = 'rgba(0, 0, 0, 1)' + +export const ROOM_DEFAULT_COLOR = 'rgba(150, 150, 150, 1)' +export const ROOM_IN_CONSTRUCTION_COLOR = 'rgba(51, 153, 255, 1)' +export const ROOM_HOVER_VALID_COLOR = 'rgba(51, 153, 255, 1)' +export const ROOM_HOVER_INVALID_COLOR = 'rgba(255, 102, 0, 1)' +export const ROOM_NAME_COLOR = 'rgba(245, 245, 245, 1)' +export const ROOM_TYPE_COLOR = 'rgba(245, 245, 245, 1)' + +export const TILE_PLUS_COLOR = 'rgba(0, 0, 0, 1)' + +export const OBJECT_BORDER_COLOR = 'rgba(0, 0, 0, 1)' + +export const RACK_BACKGROUND_COLOR = 'rgba(170, 170, 170, 1)' +export const RACK_SPACE_BAR_BACKGROUND_COLOR = 'rgba(222, 235, 247, 0.6)' +export const RACK_SPACE_BAR_FILL_COLOR = 'rgba(91, 155, 213, 0.7)' +export const RACK_ENERGY_BAR_BACKGROUND_COLOR = 'rgba(255, 242, 204, 0.6)' +export const RACK_ENERGY_BAR_FILL_COLOR = 'rgba(244, 215, 0, 0.7)' +export const COOLING_ITEM_BACKGROUND_COLOR = 'rgba(40, 50, 230, 1)' +export const PSU_BACKGROUND_COLOR = 'rgba(230, 50, 60, 1)' + +export const GRAYED_OUT_AREA_COLOR = 'rgba(0, 0, 0, 0.6)' + +export const SIM_LOW_COLOR = 'rgba(197, 224, 180, 1)' +export const SIM_MID_LOW_COLOR = 'rgba(255, 230, 153, 1)' +export const SIM_MID_HIGH_COLOR = 'rgba(248, 203, 173, 1)' +export const SIM_HIGH_COLOR = 'rgba(249, 165, 165, 1)' diff --git a/opendc-web/opendc-web-ui/src/util/date-time.js b/opendc-web/opendc-web-ui/src/util/date-time.js new file mode 100644 index 00000000..66efdf5b --- /dev/null +++ b/opendc-web/opendc-web-ui/src/util/date-time.js @@ -0,0 +1,93 @@ +/** + * Parses and formats the given date-time string representation. + * + * The format assumed is "YYYY-MM-DDTHH:MM:SS". + * + * @param dateTimeString A string expressing a date and a time, in the above mentioned format. + * @returns {string} A human-friendly string version of that date and time. + */ +export function parseAndFormatDateTime(dateTimeString) { + return formatDateTime(parseDateTime(dateTimeString)) +} + +/** + * Parses date-time string representations and returns a parsed object. + * + * The format assumed is "YYYY-MM-DDTHH:MM:SS". + * + * @param dateTimeString A string expressing a date and a time, in the above mentioned format. + * @returns {object} A Date object with the parsed date and time information as content. + */ +export function parseDateTime(dateTimeString) { + return new Date(dateTimeString + '.000Z') +} + +/** + * Serializes the given date and time value to a human-friendly string. + * + * @param dateTime An object representation of a date and time. + * @returns {string} A human-friendly string version of that date and time. + */ +export function formatDateTime(dateTime) { + let date + const currentDate = new Date() + + date = + addPaddingToTwo(dateTime.getDay()) + + '/' + + addPaddingToTwo(dateTime.getMonth()) + + '/' + + addPaddingToTwo(dateTime.getFullYear()) + + if (dateTime.getFullYear() === currentDate.getFullYear() && dateTime.getMonth() === currentDate.getMonth()) { + if (dateTime.getDate() === currentDate.getDate()) { + date = 'Today' + } else if (dateTime.getDate() === currentDate.getDate() - 1) { + date = 'Yesterday' + } + } + + return date + ', ' + addPaddingToTwo(dateTime.getHours()) + ':' + addPaddingToTwo(dateTime.getMinutes()) +} + +/** + * Formats the given number of seconds/ticks to a formatted time representation. + * + * @param seconds The number of seconds. + * @returns {string} A string representation of that amount of second, in the from of HH:MM:SS. + */ +export function convertSecondsToFormattedTime(seconds) { + if (seconds <= 0) { + return '0s' + } + + let hour = Math.floor(seconds / 3600) + let minute = Math.floor(seconds / 60) % 60 + let second = seconds % 60 + + hour = isNaN(hour) ? 0 : hour + minute = isNaN(minute) ? 0 : minute + second = isNaN(second) ? 0 : second + + if (hour === 0 && minute === 0) { + return second + 's' + } else if (hour === 0) { + return minute + 'm' + addPaddingToTwo(second) + 's' + } else { + return hour + 'h' + addPaddingToTwo(minute) + 'm' + addPaddingToTwo(second) + 's' + } +} + +/** + * Pads the given integer to have at least two digits. + * + * @param integer An integer to be padded. + * @returns {string} A string containing the padded integer. + */ +function addPaddingToTwo(integer) { + if (integer < 10) { + return '0' + integer.toString() + } else { + return integer.toString() + } +} diff --git a/opendc-web/opendc-web-ui/src/util/date-time.test.js b/opendc-web/opendc-web-ui/src/util/date-time.test.js new file mode 100644 index 00000000..3d95eba6 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/util/date-time.test.js @@ -0,0 +1,35 @@ +import { convertSecondsToFormattedTime, parseDateTime } from './date-time' + +describe('date-time parsing', () => { + it('reads components properly', () => { + const dateString = '2017-09-27T20:55:01' + const parsedDate = parseDateTime(dateString) + + expect(parsedDate.getUTCFullYear()).toEqual(2017) + expect(parsedDate.getUTCMonth()).toEqual(8) + expect(parsedDate.getUTCDate()).toEqual(27) + expect(parsedDate.getUTCHours()).toEqual(20) + expect(parsedDate.getUTCMinutes()).toEqual(55) + expect(parsedDate.getUTCSeconds()).toEqual(1) + }) +}) + +describe('tick formatting', () => { + it("returns '0s' for numbers <= 0", () => { + expect(convertSecondsToFormattedTime(-1)).toEqual('0s') + expect(convertSecondsToFormattedTime(0)).toEqual('0s') + }) + it('returns only seconds for values under a minute', () => { + expect(convertSecondsToFormattedTime(1)).toEqual('1s') + expect(convertSecondsToFormattedTime(59)).toEqual('59s') + }) + it('returns seconds and minutes for values under an hour', () => { + expect(convertSecondsToFormattedTime(60)).toEqual('1m00s') + expect(convertSecondsToFormattedTime(61)).toEqual('1m01s') + expect(convertSecondsToFormattedTime(3599)).toEqual('59m59s') + }) + it('returns full time for values over an hour', () => { + expect(convertSecondsToFormattedTime(3600)).toEqual('1h00m00s') + expect(convertSecondsToFormattedTime(3601)).toEqual('1h00m01s') + }) +}) diff --git a/opendc-web/opendc-web-ui/src/util/sidebar-space.js b/opendc-web/opendc-web-ui/src/util/sidebar-space.js new file mode 100644 index 00000000..ef09d40a --- /dev/null +++ b/opendc-web/opendc-web-ui/src/util/sidebar-space.js @@ -0,0 +1,2 @@ +export const isCollapsible = (location) => + location.pathname.indexOf('portfolios') === -1 && location.pathname.indexOf('scenarios') === -1 diff --git a/opendc-web/opendc-web-ui/src/util/state-utils.js b/opendc-web/opendc-web-ui/src/util/state-utils.js new file mode 100644 index 00000000..e5b695c3 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/util/state-utils.js @@ -0,0 +1,6 @@ +export const getState = (dispatch) => + new Promise((resolve) => { + dispatch((dispatch, getState) => { + resolve(getState()) + }) + }) diff --git a/opendc-web/opendc-web-ui/src/util/tile-calculations.js b/opendc-web/opendc-web-ui/src/util/tile-calculations.js new file mode 100644 index 00000000..764ae6ac --- /dev/null +++ b/opendc-web/opendc-web-ui/src/util/tile-calculations.js @@ -0,0 +1,255 @@ +export function deriveWallLocations(tiles) { + const { verticalWalls, horizontalWalls } = getWallSegments(tiles) + return mergeWallSegments(verticalWalls, horizontalWalls) +} + +function getWallSegments(tiles) { + const verticalWalls = {} + const horizontalWalls = {} + + tiles.forEach((tile) => { + const x = tile.positionX, + y = tile.positionY + + for (let dX = -1; dX <= 1; dX++) { + for (let dY = -1; dY <= 1; dY++) { + if (Math.abs(dX) === Math.abs(dY)) { + continue + } + + let doInsert = true + for (let tileIndex in tiles) { + if (tiles[tileIndex].positionX === x + dX && tiles[tileIndex].positionY === y + dY) { + doInsert = false + break + } + } + if (!doInsert) { + continue + } + + if (dX === -1) { + if (verticalWalls[x] === undefined) { + verticalWalls[x] = [] + } + if (verticalWalls[x].indexOf(y) === -1) { + verticalWalls[x].push(y) + } + } else if (dX === 1) { + if (verticalWalls[x + 1] === undefined) { + verticalWalls[x + 1] = [] + } + if (verticalWalls[x + 1].indexOf(y) === -1) { + verticalWalls[x + 1].push(y) + } + } else if (dY === -1) { + if (horizontalWalls[y] === undefined) { + horizontalWalls[y] = [] + } + if (horizontalWalls[y].indexOf(x) === -1) { + horizontalWalls[y].push(x) + } + } else if (dY === 1) { + if (horizontalWalls[y + 1] === undefined) { + horizontalWalls[y + 1] = [] + } + if (horizontalWalls[y + 1].indexOf(x) === -1) { + horizontalWalls[y + 1].push(x) + } + } + } + } + }) + + return { verticalWalls, horizontalWalls } +} + +function mergeWallSegments(vertical, horizontal) { + const result = [] + const walls = [vertical, horizontal] + + for (let i = 0; i < 2; i++) { + const wallList = walls[i] + for (let a in wallList) { + a = parseInt(a, 10) + + wallList[a].sort((a, b) => { + return a - b + }) + + let startPos = wallList[a][0] + const isHorizontal = i === 1 + + if (wallList[a].length === 1) { + const startPosX = isHorizontal ? startPos : a + const startPosY = isHorizontal ? a : startPos + result.push({ + startPosX, + startPosY, + isHorizontal, + length: 1, + }) + } else { + let consecutiveCount = 1 + for (let b = 0; b < wallList[a].length - 1; b++) { + if (b + 1 === wallList[a].length - 1) { + if (wallList[a][b + 1] - wallList[a][b] > 1) { + const startPosX = isHorizontal ? startPos : a + const startPosY = isHorizontal ? a : startPos + result.push({ + startPosX, + startPosY, + isHorizontal, + length: consecutiveCount, + }) + consecutiveCount = 0 + startPos = wallList[a][b + 1] + } + const startPosX = isHorizontal ? startPos : a + const startPosY = isHorizontal ? a : startPos + result.push({ + startPosX, + startPosY, + isHorizontal, + length: consecutiveCount + 1, + }) + break + } else if (wallList[a][b + 1] - wallList[a][b] > 1) { + const startPosX = isHorizontal ? startPos : a + const startPosY = isHorizontal ? a : startPos + result.push({ + startPosX, + startPosY, + isHorizontal, + length: consecutiveCount, + }) + startPos = wallList[a][b + 1] + consecutiveCount = 0 + } + consecutiveCount++ + } + } + } + } + + return result +} + +export function deriveValidNextTilePositions(rooms, selectedTiles) { + const result = [], + newPosition = { x: 0, y: 0 } + let isSurroundingTile + + selectedTiles.forEach((tile) => { + const x = tile.positionX + const y = tile.positionY + result.push({ x, y }) + + for (let dX = -1; dX <= 1; dX++) { + for (let dY = -1; dY <= 1; dY++) { + if (Math.abs(dX) === Math.abs(dY)) { + continue + } + + newPosition.x = x + dX + newPosition.y = y + dY + + isSurroundingTile = true + for (let index in selectedTiles) { + if ( + selectedTiles[index].positionX === newPosition.x && + selectedTiles[index].positionY === newPosition.y + ) { + isSurroundingTile = false + break + } + } + + if (isSurroundingTile && findPositionInRooms(rooms, newPosition.x, newPosition.y) === -1) { + result.push({ x: newPosition.x, y: newPosition.y }) + } + } + } + }) + + return result +} + +export function findPositionInPositions(positions, positionX, positionY) { + for (let i = 0; i < positions.length; i++) { + const position = positions[i] + if (positionX === position.x && positionY === position.y) { + return i + } + } + + return -1 +} + +export function findPositionInRooms(rooms, positionX, positionY) { + for (let i = 0; i < rooms.length; i++) { + const room = rooms[i] + if (findPositionInTiles(room.tiles, positionX, positionY) !== -1) { + return i + } + } + + return -1 +} + +function findPositionInTiles(tiles, positionX, positionY) { + let index = -1 + + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i] + if (positionX === tile.positionX && positionY === tile.positionY) { + index = i + break + } + } + + return index +} + +export function findTileWithPosition(tiles, positionX, positionY) { + for (let i = 0; i < tiles.length; i++) { + if (tiles[i].positionX === positionX && tiles[i].positionY === positionY) { + return tiles[i] + } + } + + return null +} + +export function calculateRoomListBounds(rooms) { + const min = { x: Number.MAX_VALUE, y: Number.MAX_VALUE } + const max = { x: -1, y: -1 } + + rooms.forEach((room) => { + room.tiles.forEach((tile) => { + if (tile.positionX < min.x) { + min.x = tile.positionX + } + if (tile.positionY < min.y) { + min.y = tile.positionY + } + + if (tile.positionX > max.x) { + max.x = tile.positionX + } + if (tile.positionY > max.y) { + max.y = tile.positionY + } + }) + }) + + max.x++ + max.y++ + + const center = { + x: min.x + (max.x - min.x) / 2.0, + y: min.y + (max.y - min.y) / 2.0, + } + + return { min, center, max } +} diff --git a/opendc-web/opendc-web-ui/src/util/timeline.js b/opendc-web/opendc-web-ui/src/util/timeline.js new file mode 100644 index 00000000..7c8a3ef0 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/util/timeline.js @@ -0,0 +1,9 @@ +export function convertTickToPercentage(tick, maxTick) { + if (maxTick === 0) { + return '0%' + } else if (tick > maxTick) { + return (maxTick / (maxTick + 1)) * 100 + '%' + } + + return (tick / (maxTick + 1)) * 100 + '%' +} diff --git a/opendc-web/opendc-web-ui/src/util/unit-specifications.js b/opendc-web/opendc-web-ui/src/util/unit-specifications.js new file mode 100644 index 00000000..28479edd --- /dev/null +++ b/opendc-web/opendc-web-ui/src/util/unit-specifications.js @@ -0,0 +1,102 @@ +export const CPU_UNITS = { + 'cpu-1': { + _id: 'cpu-1', + name: 'Intel i7 v6 6700k', + clockRateMhz: 4100, + numberOfCores: 4, + energyConsumptionW: 70, + }, + 'cpu-2': { + _id: 'cpu-2', + name: 'Intel i5 v6 6700k', + clockRateMhz: 3500, + numberOfCores: 2, + energyConsumptionW: 50, + }, + 'cpu-3': { + _id: 'cpu-3', + name: 'Intel® Xeon® E-2224G', + clockRateMhz: 3500, + numberOfCores: 4, + energyConsumptionW: 71, + }, + 'cpu-4': { + _id: 'cpu-4', + name: 'Intel® Xeon® E-2244G', + clockRateMhz: 3800, + numberOfCores: 8, + energyConsumptionW: 71, + }, + 'cpu-5': { + _id: 'cpu-5', + name: 'Intel® Xeon® E-2246G', + clockRateMhz: 3600, + numberOfCores: 12, + energyConsumptionW: 80, + }, +} + +export const GPU_UNITS = { + 'gpu-1': { + _id: 'gpu-1', + name: 'NVIDIA GTX 4 1080', + clockRateMhz: 1200, + numberOfCores: 200, + energyConsumptionW: 250, + }, + 'gpu-2': { + _id: 'gpu-2', + name: 'NVIDIA Tesla V100', + clockRateMhz: 1200, + numberOfCores: 5120, + energyConsumptionW: 250, + }, +} + +export const MEMORY_UNITS = { + 'memory-1': { + _id: 'memory-1', + name: 'Samsung PC DRAM K4A4G045WD', + speedMbPerS: 16000, + sizeMb: 4000, + energyConsumptionW: 10, + }, + 'memory-2': { + _id: 'memory-2', + name: 'Samsung PC DRAM M393A2K43BB1-CRC', + speedMbPerS: 2400, + sizeMb: 16000, + energyConsumptionW: 10, + }, + 'memory-3': { + _id: 'memory-3', + name: 'Crucial MTA18ASF4G72PDZ-3G2E1', + speedMbPerS: 3200, + sizeMb: 32000, + energyConsumptionW: 10, + }, + 'memory-4': { + _id: 'memory-4', + name: 'Crucial MTA9ASF2G72PZ-3G2E1', + speedMbPerS: 3200, + sizeMb: 16000, + energyConsumptionW: 10, + }, +} + +export const STORAGE_UNITS = { + 'storage-1': { + _id: 'storage-1', + name: 'Samsung EVO 2016 SATA III', + speedMbPerS: 6000, + sizeMb: 250000, + energyConsumptionW: 10, + }, + 'storage-2': { + _id: 'storage-2', + name: 'Western Digital MTA9ASF2G72PZ-3G2E1', + speedMbPerS: 6000, + sizeMb: 4000000, + energyConsumptionW: 10, + }, +} |
