diff options
| author | Georgios Andreadis <g.andreadis@student.tudelft.nl> | 2017-08-31 17:59:51 +0200 |
|---|---|---|
| committer | Georgios Andreadis <g.andreadis@student.tudelft.nl> | 2017-09-23 10:05:50 +0200 |
| commit | 3f736cd3db63f106eac02f220477b4a0f3b0eceb (patch) | |
| tree | 80afa73f8c4d281b2fccba8ad2baa7c10f7e7e84 | |
| parent | b17f1d8cb4815f57a4b7043cc91b867ec3cbc867 (diff) | |
Implement room creation
39 files changed, 603 insertions, 82 deletions
diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 00000000..6233fae0 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "OpenDC", + "name": "OpenDC", + "icons": [ + { + "src": "favicon.ico", + "sizes": "192x192", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#00A6D6", + "background_color": "#eeeeee" +} diff --git a/src/actions/objects.js b/src/actions/objects.js index 6fdb83cd..60885471 100644 --- a/src/actions/objects.js +++ b/src/actions/objects.js @@ -1,5 +1,7 @@ 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 { @@ -17,3 +19,23 @@ export function addPropToStoreObject(objectType, objectId, propObject) { 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/src/actions/topology.js b/src/actions/topology.js index de742bb1..76b1ef27 100644 --- a/src/actions/topology.js +++ b/src/actions/topology.js @@ -1,7 +1,18 @@ +import {addIdToStoreObjectListProp, removeIdFromStoreObjectListProp} from "./objects"; + export const FETCH_TOPOLOGY_OF_DATACENTER = "FETCH_TOPOLOGY_OF_DATACENTER"; export const FETCH_TOPOLOGY_OF_DATACENTER_SUCCEEDED = "FETCH_TOPOLOGY_OF_DATACENTER_SUCCEEDED"; export const FETCH_LATEST_DATACENTER = "FETCH_LATEST_DATACENTER"; export const FETCH_LATEST_DATACENTER_SUCCEEDED = "FETCH_LATEST_DATACENTER_SUCCEEDED"; +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 ADD_TILE = "ADD_TILE"; +export const ADD_TILE_SUCCEEDED = "ADD_TILE_SUCCEEDED"; +export const DELETE_TILE = "DELETE_TILE"; +export const DELETE_TILE_SUCCEEDED = "DELETE_TILE_SUCCEEDED"; export function fetchLatestDatacenter() { return (dispatch, getState) => { @@ -19,3 +30,104 @@ export function fetchLatestDatacenterSucceeded(datacenterId) { datacenterId }; } + +export function startNewRoomConstruction() { + return { + type: START_NEW_ROOM_CONSTRUCTION + }; +} + +export function startNewRoomConstructionSucceeded(roomId) { + return (dispatch, getState) => { + const {currentDatacenterId} = getState(); + dispatch(addIdToStoreObjectListProp("datacenter", currentDatacenterId, "roomIds", roomId)); + dispatch({ + type: START_NEW_ROOM_CONSTRUCTION_SUCCEEDED, + roomId + }); + }; +} + +export function finishNewRoomConstruction() { + return (dispatch, getState) => { + const {objects, currentRoomInConstruction} = getState(); + if (objects.room[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 (dispatch, getState) => { + const {currentDatacenterId, currentRoomInConstruction} = getState(); + dispatch(removeIdFromStoreObjectListProp("datacenter", currentDatacenterId, "roomIds", + currentRoomInConstruction)); + dispatch({ + type: CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED + }); + }; +} + +export function toggleTileAtLocation(positionX, positionY) { + return (dispatch, getState) => { + const {objects, currentRoomInConstruction} = getState(); + + const tileIds = objects.room[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 addTileSucceeded(tileId) { + return (dispatch, getState) => { + const {currentRoomInConstruction} = getState(); + dispatch(addIdToStoreObjectListProp("room", currentRoomInConstruction, "tileIds", tileId)); + dispatch({ + type: ADD_TILE_SUCCEEDED, + tileId + }); + }; +} + +export function deleteTile(tileId) { + return { + type: DELETE_TILE, + tileId + } +} + +export function deleteTileSucceeded(tileId) { + return (dispatch, getState) => { + const {currentRoomInConstruction} = getState(); + dispatch(removeIdFromStoreObjectListProp("room", currentRoomInConstruction, "tileIds", tileId)); + dispatch({ + type: DELETE_TILE_SUCCEEDED, + tileId + }); + }; +} diff --git a/src/api/routes/token-signin.js b/src/api/routes/token-signin.js index 76a39572..dbf70869 100644 --- a/src/api/routes/token-signin.js +++ b/src/api/routes/token-signin.js @@ -1,5 +1,5 @@ export function performTokenSignIn(token) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { window["jQuery"].post( "/tokensignin", {idtoken: token}, diff --git a/src/colors/index.js b/src/colors/index.js index cc5b86fe..22ce9feb 100644 --- a/src/colors/index.js +++ b/src/colors/index.js @@ -3,7 +3,7 @@ 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_SELECTED_COLOR = "rgba(51, 153, 255, 1)"; +export const ROOM_IN_CONSTRUCTION_COLOR = "rgba(51, 153, 255, 1)"; export const ROOM_HOVER_VALID_COLOR = "rgba(51, 153, 255, 0.5)"; export const ROOM_HOVER_INVALID_COLOR = "rgba(255, 102, 0, 0.5)"; export const ROOM_NAME_COLOR = "rgba(245, 245, 245, 1)"; diff --git a/src/components/map/MapStage.js b/src/components/map/MapStage.js index 644b4c54..541df1d6 100644 --- a/src/components/map/MapStage.js +++ b/src/components/map/MapStage.js @@ -1,6 +1,7 @@ import React from "react"; import {Group, Layer, Stage} from "react-konva"; import DatacenterContainer from "../../containers/map/DatacenterContainer"; +import HoverTileLayer from "../../containers/map/layers/HoverTileLayer"; import jQuery from "../../util/jquery"; import {NAVBAR_HEIGHT} from "../navigation/Navbar"; import Backdrop from "./elements/Backdrop"; @@ -10,7 +11,11 @@ import {MAP_SIZE_IN_PIXELS} from "./MapConstants"; class MapStage extends React.Component { state = { width: 600, - height: 400 + height: 400, + x: 0, + y: 0, + mouseX: 0, + mouseY: 0 }; componentWillMount() { @@ -29,18 +34,30 @@ class MapStage extends React.Component { this.setState({width: jQuery(window).width(), height: jQuery(window).height() - NAVBAR_HEIGHT}); } + updateMousePosition() { + const mousePos = this.stage.getStage().getPointerPosition(); + this.setState({mouseX: mousePos.x, mouseY: mousePos.y}); + } + dragBoundFunc(pos) { - return { + const updatedPosition = { x: pos.x > 0 ? 0 : (pos.x < -MAP_SIZE_IN_PIXELS + this.state.width ? -MAP_SIZE_IN_PIXELS + this.state.width : pos.x), y: pos.y > 0 ? 0 : (pos.y < -MAP_SIZE_IN_PIXELS + this.state.height ? -MAP_SIZE_IN_PIXELS + this.state.height : pos.y) - } + }; + + this.setState(updatedPosition); + + return updatedPosition; } render() { return ( - <Stage width={this.state.width} height={this.state.height}> + <Stage ref={(stage) => {this.stage = stage;}} + width={this.state.width} + height={this.state.height} + onMouseMove={this.updateMousePosition.bind(this)}> <Layer> <Group draggable={true} dragBoundFunc={this.dragBoundFunc.bind(this)}> <Backdrop/> @@ -48,6 +65,12 @@ class MapStage extends React.Component { <GridGroup/> </Group> </Layer> + <HoverTileLayer + mainGroupX={this.state.x} + mainGroupY={this.state.y} + mouseX={this.state.mouseX} + mouseY={this.state.mouseY} + /> </Stage> ) } diff --git a/src/components/map/elements/HoverTile.js b/src/components/map/elements/HoverTile.js new file mode 100644 index 00000000..a8bb2085 --- /dev/null +++ b/src/components/map/elements/HoverTile.js @@ -0,0 +1,25 @@ +import PropTypes from "prop-types"; +import React from "react"; +import {Rect} from "react-konva"; +import {ROOM_HOVER_INVALID_COLOR, ROOM_HOVER_VALID_COLOR} from "../../../colors/index"; +import {TILE_SIZE_IN_PIXELS} from "../MapConstants"; + +const HoverTile = ({pixelX, pixelY, isValid, onClick}) => ( + <Rect + x={pixelX} + y={pixelY} + 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/src/components/map/elements/RoomTile.js b/src/components/map/elements/RoomTile.js index 759dcf35..4d5e50fc 100644 --- a/src/components/map/elements/RoomTile.js +++ b/src/components/map/elements/RoomTile.js @@ -1,16 +1,16 @@ import React from "react"; import {Rect} from "react-konva"; -import {ROOM_DEFAULT_COLOR} from "../../../colors/index"; +import {ROOM_DEFAULT_COLOR, ROOM_IN_CONSTRUCTION_COLOR} from "../../../colors/index"; import Shapes from "../../../shapes/index"; import {TILE_SIZE_IN_PIXELS} from "../MapConstants"; -const RoomTile = ({tile}) => ( +const RoomTile = ({tile, newTile}) => ( <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={ROOM_DEFAULT_COLOR} + fill={newTile ? ROOM_IN_CONSTRUCTION_COLOR : ROOM_DEFAULT_COLOR} /> ); diff --git a/src/components/map/groups/DatacenterGroup.js b/src/components/map/groups/DatacenterGroup.js index 2f4dc2eb..0830ac4e 100644 --- a/src/components/map/groups/DatacenterGroup.js +++ b/src/components/map/groups/DatacenterGroup.js @@ -12,8 +12,8 @@ const DatacenterGroup = ({datacenter, interactionLevel}) => { if (interactionLevel.mode === "BUILDING") { return ( <Group> - {datacenter.rooms.map(room => ( - <RoomContainer key={room.id} room={room}/> + {datacenter.roomIds.map(roomId => ( + <RoomContainer key={roomId} roomId={roomId}/> ))} </Group> ); @@ -21,14 +21,14 @@ const DatacenterGroup = ({datacenter, interactionLevel}) => { return ( <Group> - {datacenter.rooms - .filter(room => room.id !== interactionLevel.roomId) - .map(room => <RoomContainer key={room.id} room={room}/>) + {datacenter.roomIds + .filter(roomId => roomId !== interactionLevel.roomId) + .map(roomId => <RoomContainer key={roomId} roomId={roomId}/>) } {interactionLevel.mode === "ROOM" ? <GrayContainer/> : null} - {datacenter.rooms - .filter(room => room.id === interactionLevel.roomId) - .map(room => <RoomContainer key={room.id} room={room}/>) + {datacenter.roomIds + .filter(roomId => roomId === interactionLevel.roomId) + .map(roomId => <RoomContainer key={roomId} roomId={roomId}/>) } </Group> ); diff --git a/src/components/map/groups/RoomGroup.js b/src/components/map/groups/RoomGroup.js index b6c285e9..85d3c7c9 100644 --- a/src/components/map/groups/RoomGroup.js +++ b/src/components/map/groups/RoomGroup.js @@ -2,35 +2,40 @@ import React from "react"; import {Group} from "react-konva"; import GrayContainer from "../../../containers/map/GrayContainer"; import TileContainer from "../../../containers/map/TileContainer"; +import WallContainer from "../../../containers/map/WallContainer"; import Shapes from "../../../shapes/index"; -import {deriveWallLocations} from "../../../util/tile-calculations"; -import WallSegment from "../elements/WallSegment"; -const RoomGroup = ({room, interactionLevel, onClick}) => { +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} - > + <Group onClick={onClick}> {(() => { if (interactionLevel.mode === "OBJECT" && interactionLevel.roomId === room.id) { return [ - room.tiles - .filter(tile => tile.id !== interactionLevel.tileId) - .map(tile => <TileContainer key={tile.id} tile={tile}/>), + room.tileIds + .filter(tileId => tileId !== interactionLevel.tileId) + .map(tileId => <TileContainer key={tileId} tileId={tileId}/>), <GrayContainer key={-1}/>, - room.tiles - .filter(tile => tile.id === interactionLevel.tileId) - .map(tile => <TileContainer key={tile.id} tile={tile}/>) + room.tileIds + .filter(tileId => tileId === interactionLevel.tileId) + .map(tileId => <TileContainer key={tileId} tileId={tileId}/>) ]; } else { - return room.tiles.map(tile => ( - <TileContainer key={tile.id} tile={tile}/> + return room.tileIds.map(tileId => ( + <TileContainer key={tileId} tileId={tileId}/> )); } })()} - {deriveWallLocations(room).map((wallSegment, index) => ( - <WallSegment key={index} wallSegment={wallSegment}/> - ))} + <WallContainer roomId={room.id}/> </Group> ); }; diff --git a/src/components/map/groups/TileGroup.js b/src/components/map/groups/TileGroup.js index 5920a2b6..8fbdeb31 100644 --- a/src/components/map/groups/TileGroup.js +++ b/src/components/map/groups/TileGroup.js @@ -1,10 +1,11 @@ +import PropTypes from "prop-types"; import React from "react"; import {Group} from "react-konva"; import Shapes from "../../../shapes/index"; import RoomTile from "../elements/RoomTile"; import RackGroup from "./RackGroup"; -const TileGroup = ({tile, onClick}) => { +const TileGroup = ({tile, newTile, onClick}) => { let tileObject; switch (tile.objectType) { case "RACK": @@ -16,9 +17,9 @@ const TileGroup = ({tile, onClick}) => { return ( <Group - onClick={onClick} + onClick={() => onClick(tile)} > - <RoomTile tile={tile}/> + <RoomTile tile={tile} newTile={newTile}/> {tileObject} </Group> ); @@ -26,6 +27,7 @@ const TileGroup = ({tile, onClick}) => { TileGroup.propTypes = { tile: Shapes.Tile, + newTile: PropTypes.bool, }; export default TileGroup; diff --git a/src/components/map/groups/WallGroup.js b/src/components/map/groups/WallGroup.js new file mode 100644 index 00000000..f21d91a5 --- /dev/null +++ b/src/components/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/src/components/map/layers/HoverTileLayerComponent.js b/src/components/map/layers/HoverTileLayerComponent.js new file mode 100644 index 00000000..691b9733 --- /dev/null +++ b/src/components/map/layers/HoverTileLayerComponent.js @@ -0,0 +1,27 @@ +import React from 'react'; +import {Layer} from "react-konva"; +import HoverTile from "../elements/HoverTile"; +import {TILE_SIZE_IN_PIXELS} from "../MapConstants"; + +const HoverTileLayerComponent = ({mainGroupX, mainGroupY, mouseX, mouseY, currentRoomInConstruction, isValid, onClick}) => { + if (currentRoomInConstruction === -1) { + return <Layer/> + } + + const positionX = Math.floor((mouseX - mainGroupX) / TILE_SIZE_IN_PIXELS); + const positionY = Math.floor((mouseY - mainGroupY) / TILE_SIZE_IN_PIXELS); + const pixelX = positionX * TILE_SIZE_IN_PIXELS + mainGroupX; + const pixelY = positionY * TILE_SIZE_IN_PIXELS + mainGroupY; + const validity = isValid(positionX, positionY); + + return ( + <Layer> + <HoverTile + pixelX={pixelX} pixelY={pixelY} + isValid={validity} onClick={() => onClick(positionX, positionY)} + /> + </Layer> + ); +}; + +export default HoverTileLayerComponent; diff --git a/src/components/sidebars/BuildingSidebarContent.js b/src/components/sidebars/BuildingSidebarContent.js deleted file mode 100644 index 44688bb8..00000000 --- a/src/components/sidebars/BuildingSidebarContent.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; - -const BuildingSidebarContent = () => { - return ( - <p>Test</p> - ); -}; - -export default BuildingSidebarContent; diff --git a/src/components/sidebars/TopologySidebarComponent.js b/src/components/sidebars/topology/TopologySidebarComponent.js index 371463d1..932d2ecf 100644 --- a/src/components/sidebars/TopologySidebarComponent.js +++ b/src/components/sidebars/topology/TopologySidebarComponent.js @@ -1,6 +1,6 @@ import React from "react"; -import BuildingSidebarContent from "./BuildingSidebarContent"; -import Sidebar from "./Sidebar"; +import BuildingSidebarContent from "../../../containers/sidebars/topology/building/BuildingSidebarContent"; +import Sidebar from "../Sidebar"; const TopologySidebarComponent = ({interactionLevel}) => { let sidebarHeading; diff --git a/src/components/sidebars/topology/building/BuildingSidebarContentComponent.js b/src/components/sidebars/topology/building/BuildingSidebarContentComponent.js new file mode 100644 index 00000000..b88b23b7 --- /dev/null +++ b/src/components/sidebars/topology/building/BuildingSidebarContentComponent.js @@ -0,0 +1,20 @@ +import React from "react"; +import CancelNewRoomConstructionButton from "../../../../containers/sidebars/topology/building/CancelNewRoomConstructionButton"; +import FinishNewRoomConstructionButton from "../../../../containers/sidebars/topology/building/FinishNewRoomConstructionButton"; +import StartNewRoomConstructionButton from "../../../../containers/sidebars/topology/building/StartNewRoomConstructionButton"; + +const BuildingSidebarContentComponent = ({currentRoomInConstruction}) => { + if (currentRoomInConstruction !== -1) { + return ( + <div> + <FinishNewRoomConstructionButton/> + <CancelNewRoomConstructionButton/> + </div> + ); + } + return ( + <StartNewRoomConstructionButton/> + ); +}; + +export default BuildingSidebarContentComponent; diff --git a/src/components/sidebars/topology/building/CancelNewRoomConstructionComponent.js b/src/components/sidebars/topology/building/CancelNewRoomConstructionComponent.js new file mode 100644 index 00000000..15f199a6 --- /dev/null +++ b/src/components/sidebars/topology/building/CancelNewRoomConstructionComponent.js @@ -0,0 +1,9 @@ +import React from "react"; + +const CancelNewRoomConstructionComponent = ({onClick}) => ( + <div className="btn btn-default btn-block" onClick={onClick}> + Cancel construction + </div> +); + +export default CancelNewRoomConstructionComponent; diff --git a/src/components/sidebars/topology/building/FinishNewRoomConstructionComponent.js b/src/components/sidebars/topology/building/FinishNewRoomConstructionComponent.js new file mode 100644 index 00000000..d9edbb61 --- /dev/null +++ b/src/components/sidebars/topology/building/FinishNewRoomConstructionComponent.js @@ -0,0 +1,9 @@ +import React from "react"; + +const FinishNewRoomConstructionComponent = ({onClick}) => ( + <div className="btn btn-primary btn-block" onClick={onClick}> + Finalize new room + </div> +); + +export default FinishNewRoomConstructionComponent; diff --git a/src/components/sidebars/topology/building/StartNewRoomConstructionComponent.js b/src/components/sidebars/topology/building/StartNewRoomConstructionComponent.js new file mode 100644 index 00000000..60573532 --- /dev/null +++ b/src/components/sidebars/topology/building/StartNewRoomConstructionComponent.js @@ -0,0 +1,9 @@ +import React from "react"; + +const StartNewRoomConstructionComponent = ({onClick}) => ( + <div className="btn btn-primary btn-block" onClick={onClick}> + Construct a new room + </div> +); + +export default StartNewRoomConstructionComponent; diff --git a/src/containers/map/DatacenterContainer.js b/src/containers/map/DatacenterContainer.js index 00b6e79f..8c80146d 100644 --- a/src/containers/map/DatacenterContainer.js +++ b/src/containers/map/DatacenterContainer.js @@ -1,16 +1,13 @@ import {connect} from "react-redux"; import DatacenterGroup from "../../components/map/groups/DatacenterGroup"; -import {denormalize} from "../../store/denormalizer"; const mapStateToProps = state => { if (state.currentDatacenterId === -1) { return {}; } - const datacenter = denormalize(state, "datacenter", state.currentDatacenterId); - return { - datacenter, + datacenter: state.objects.datacenter[state.currentDatacenterId], interactionLevel: state.interactionLevel }; }; diff --git a/src/containers/map/RoomContainer.js b/src/containers/map/RoomContainer.js index d46324b2..2d078e09 100644 --- a/src/containers/map/RoomContainer.js +++ b/src/containers/map/RoomContainer.js @@ -2,15 +2,17 @@ import {connect} from "react-redux"; import {goFromBuildingToRoom} from "../../actions/interaction-level"; import RoomGroup from "../../components/map/groups/RoomGroup"; -const mapStateToProps = state => { +const mapStateToProps = (state, ownProps) => { return { - interactionLevel: state.interactionLevel + interactionLevel: state.interactionLevel, + currentRoomInConstruction: state.currentRoomInConstruction, + room: state.objects.room[ownProps.roomId], }; }; const mapDispatchToProps = (dispatch, ownProps) => { return { - onClick: () => dispatch(goFromBuildingToRoom(ownProps.room.id)) + onClick: () => dispatch(goFromBuildingToRoom(ownProps.roomId)), }; }; diff --git a/src/containers/map/TileContainer.js b/src/containers/map/TileContainer.js index 0456c423..d00c3611 100644 --- a/src/containers/map/TileContainer.js +++ b/src/containers/map/TileContainer.js @@ -2,17 +2,18 @@ import {connect} from "react-redux"; import {goFromRoomToObject} from "../../actions/interaction-level"; import TileGroup from "../../components/map/groups/TileGroup"; -const mapStateToProps = state => { +const mapStateToProps = (state, ownProps) => { return { - interactionLevel: state.interactionLevel + interactionLevel: state.interactionLevel, + tile: state.objects.tile[ownProps.tileId], }; }; -const mapDispatchToProps = (dispatch, ownProps) => { +const mapDispatchToProps = dispatch => { return { - onClick: () => { - if (ownProps.tile.objectType) { - dispatch(goFromRoomToObject(ownProps.tile.id)) + onClick: tile => { + if (tile.objectType) { + dispatch(goFromRoomToObject(tile.id)) } } }; diff --git a/src/containers/map/WallContainer.js b/src/containers/map/WallContainer.js new file mode 100644 index 00000000..f8ccb2e9 --- /dev/null +++ b/src/containers/map/WallContainer.js @@ -0,0 +1,14 @@ +import {connect} from "react-redux"; +import WallGroup from "../../components/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/src/containers/map/layers/HoverTileLayer.js b/src/containers/map/layers/HoverTileLayer.js new file mode 100644 index 00000000..b8868233 --- /dev/null +++ b/src/containers/map/layers/HoverTileLayer.js @@ -0,0 +1,23 @@ +import {connect} from "react-redux"; +import {toggleTileAtLocation} from "../../../actions/topology"; +import HoverTileLayerComponent from "../../../components/map/layers/HoverTileLayerComponent"; + +const mapStateToProps = state => { + return { + currentRoomInConstruction: state.currentRoomInConstruction, + isValid: (x, y) => true, // TODO implement proper validation + }; +}; + +const mapDispatchToProps = dispatch => { + return { + onClick: (x, y) => dispatch(toggleTileAtLocation(x, y)), + }; +}; + +const HoverTileLayer = connect( + mapStateToProps, + mapDispatchToProps +)(HoverTileLayerComponent); + +export default HoverTileLayer; diff --git a/src/containers/sidebars/TopologySidebar.js b/src/containers/sidebars/topology/TopologySidebar.js index 1909443a..6ed836da 100644 --- a/src/containers/sidebars/TopologySidebar.js +++ b/src/containers/sidebars/topology/TopologySidebar.js @@ -1,5 +1,5 @@ import {connect} from "react-redux"; -import TopologySidebarComponent from "../../components/sidebars/TopologySidebarComponent"; +import TopologySidebarComponent from "../../../components/sidebars/topology/TopologySidebarComponent"; const mapStateToProps = state => { return { diff --git a/src/containers/sidebars/topology/building/BuildingSidebarContent.js b/src/containers/sidebars/topology/building/BuildingSidebarContent.js new file mode 100644 index 00000000..c70c6982 --- /dev/null +++ b/src/containers/sidebars/topology/building/BuildingSidebarContent.js @@ -0,0 +1,14 @@ +import {connect} from "react-redux"; +import BuildingSidebarContentComponent from "../../../../components/sidebars/topology/building/BuildingSidebarContentComponent"; + +const mapStateToProps = state => { + return { + currentRoomInConstruction: state.currentRoomInConstruction + }; +}; + +const BuildingSidebarContent = connect( + mapStateToProps +)(BuildingSidebarContentComponent); + +export default BuildingSidebarContent; diff --git a/src/containers/sidebars/topology/building/CancelNewRoomConstructionButton.js b/src/containers/sidebars/topology/building/CancelNewRoomConstructionButton.js new file mode 100644 index 00000000..6061da96 --- /dev/null +++ b/src/containers/sidebars/topology/building/CancelNewRoomConstructionButton.js @@ -0,0 +1,16 @@ +import {connect} from "react-redux"; +import {cancelNewRoomConstruction} from "../../../../actions/topology"; +import CancelNewRoomConstructionComponent from "../../../../components/sidebars/topology/building/CancelNewRoomConstructionComponent"; + +const mapDispatchToProps = dispatch => { + return { + onClick: () => dispatch(cancelNewRoomConstruction()), + }; +}; + +const CancelNewRoomConstructionButton = connect( + null, + mapDispatchToProps +)(CancelNewRoomConstructionComponent); + +export default CancelNewRoomConstructionButton; diff --git a/src/containers/sidebars/topology/building/FinishNewRoomConstructionButton.js b/src/containers/sidebars/topology/building/FinishNewRoomConstructionButton.js new file mode 100644 index 00000000..ca34dcc3 --- /dev/null +++ b/src/containers/sidebars/topology/building/FinishNewRoomConstructionButton.js @@ -0,0 +1,16 @@ +import {connect} from "react-redux"; +import {finishNewRoomConstruction} from "../../../../actions/topology"; +import FinishNewRoomConstructionComponent from "../../../../components/sidebars/topology/building/FinishNewRoomConstructionComponent"; + +const mapDispatchToProps = dispatch => { + return { + onClick: () => dispatch(finishNewRoomConstruction()), + }; +}; + +const FinishNewRoomConstructionButton = connect( + null, + mapDispatchToProps +)(FinishNewRoomConstructionComponent); + +export default FinishNewRoomConstructionButton; diff --git a/src/containers/sidebars/topology/building/StartNewRoomConstructionButton.js b/src/containers/sidebars/topology/building/StartNewRoomConstructionButton.js new file mode 100644 index 00000000..f26eb5d4 --- /dev/null +++ b/src/containers/sidebars/topology/building/StartNewRoomConstructionButton.js @@ -0,0 +1,16 @@ +import {connect} from "react-redux"; +import {startNewRoomConstruction} from "../../../../actions/topology"; +import StartNewRoomConstructionComponent from "../../../../components/sidebars/topology/building/StartNewRoomConstructionComponent"; + +const mapDispatchToProps = dispatch => { + return { + onClick: () => dispatch(startNewRoomConstruction()), + }; +}; + +const StartNewRoomConstructionButton = connect( + null, + mapDispatchToProps +)(StartNewRoomConstructionComponent); + +export default StartNewRoomConstructionButton; diff --git a/src/containers/simulations/SimulationActions.js b/src/containers/simulations/SimulationActions.js index 01ccaa91..4dc93c77 100644 --- a/src/containers/simulations/SimulationActions.js +++ b/src/containers/simulations/SimulationActions.js @@ -10,7 +10,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - onViewUsers: (id) => {}, + onViewUsers: (id) => {}, // TODO implement user viewing onDelete: (id) => dispatch(deleteSimulation(id)), }; }; diff --git a/src/index.sass b/src/index.sass index 18d14843..4b36d4e9 100644 --- a/src/index.sass +++ b/src/index.sass @@ -1,3 +1,5 @@ +@import ./style-globals/_mixins.sass + html, body, #root margin: 0 padding: 0 @@ -17,5 +19,8 @@ html, body, #root .text-page-container padding-top: 80px +.btn + +clickable + a, a:hover text-decoration: none diff --git a/src/pages/App.js b/src/pages/App.js index 4f973ab7..a6f4dad4 100644 --- a/src/pages/App.js +++ b/src/pages/App.js @@ -5,7 +5,7 @@ import {openSimulationSucceeded} from "../actions/simulations"; import {fetchLatestDatacenter} from "../actions/topology"; import MapStage from "../components/map/MapStage"; import AppNavbar from "../components/navigation/AppNavbar"; -import TopologySidebar from "../containers/sidebars/TopologySidebar"; +import TopologySidebar from "../containers/sidebars/topology/TopologySidebar"; class AppContainer extends React.Component { static propTypes = { diff --git a/src/reducers/index.js b/src/reducers/index.js index 4ddaaec9..d3ace393 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -4,7 +4,7 @@ import {interactionLevel} from "./interaction-level"; import {modals} from "./modals"; import {objects} from "./objects"; import {authorizationsOfCurrentUser, authVisibilityFilter, currentSimulationId} from "./simulations"; -import {currentDatacenterId} from "./topology"; +import {currentDatacenterId, currentRoomInConstruction} from "./topology"; const rootReducer = combineReducers({ auth, @@ -15,6 +15,7 @@ const rootReducer = combineReducers({ currentSimulationId, currentDatacenterId, interactionLevel, + currentRoomInConstruction, }); export default rootReducer; diff --git a/src/reducers/objects.js b/src/reducers/objects.js index 4fbffea6..801a5456 100644 --- a/src/reducers/objects.js +++ b/src/reducers/objects.js @@ -1,5 +1,10 @@ import {combineReducers} from "redux"; -import {ADD_PROP_TO_STORE_OBJECT, ADD_TO_STORE} from "../actions/objects"; +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"; export const objects = combineReducers({ simulation: object("simulation"), @@ -33,14 +38,40 @@ function objectWithId(type, getId) { 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/src/reducers/topology.js b/src/reducers/topology.js index caafb7c1..c8690816 100644 --- a/src/reducers/topology.js +++ b/src/reducers/topology.js @@ -1,4 +1,9 @@ -import {FETCH_LATEST_DATACENTER_SUCCEEDED} from "../actions/topology"; +import { + CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED, + FETCH_LATEST_DATACENTER_SUCCEEDED, + FINISH_NEW_ROOM_CONSTRUCTION, + START_NEW_ROOM_CONSTRUCTION_SUCCEEDED +} from "../actions/topology"; export function currentDatacenterId(state = -1, action) { switch (action.type) { @@ -8,3 +13,15 @@ export function currentDatacenterId(state = -1, action) { return state; } } + +export function currentRoomInConstruction(state = -1, action) { + switch (action.type) { + case START_NEW_ROOM_CONSTRUCTION_SUCCEEDED: + return action.roomId; + case CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED: + case FINISH_NEW_ROOM_CONSTRUCTION: + return -1; + default: + return state; + } +} diff --git a/src/sagas/index.js b/src/sagas/index.js index c6177cd4..7c1d921f 100644 --- a/src/sagas/index.js +++ b/src/sagas/index.js @@ -1,11 +1,23 @@ import {takeEvery} from "redux-saga/effects"; import {LOG_IN} from "../actions/auth"; import {ADD_SIMULATION, DELETE_SIMULATION} from "../actions/simulations"; -import {FETCH_LATEST_DATACENTER} from "../actions/topology"; +import { + ADD_TILE, + CANCEL_NEW_ROOM_CONSTRUCTION, + DELETE_TILE, + FETCH_LATEST_DATACENTER, + START_NEW_ROOM_CONSTRUCTION +} from "../actions/topology"; import {DELETE_CURRENT_USER, FETCH_AUTHORIZATIONS_OF_CURRENT_USER} from "../actions/users"; import {onDeleteCurrentUser} from "./profile"; import {onSimulationAdd, onSimulationDelete} from "./simulations"; -import {onFetchLatestDatacenter} from "./topology"; +import { + onAddTile, + onCancelNewRoomConstruction, + onDeleteTile, + onFetchLatestDatacenter, + onStartNewRoomConstruction +} from "./topology"; import {onFetchAuthorizationsOfCurrentUser, onFetchLoggedInUser} from "./users"; export default function* rootSaga() { @@ -15,4 +27,8 @@ export default function* rootSaga() { yield takeEvery(DELETE_SIMULATION, onSimulationDelete); yield takeEvery(DELETE_CURRENT_USER, onDeleteCurrentUser); yield takeEvery(FETCH_LATEST_DATACENTER, onFetchLatestDatacenter); + yield takeEvery(START_NEW_ROOM_CONSTRUCTION, onStartNewRoomConstruction); + yield takeEvery(CANCEL_NEW_ROOM_CONSTRUCTION, onCancelNewRoomConstruction); + yield takeEvery(ADD_TILE, onAddTile); + yield takeEvery(DELETE_TILE, onDeleteTile); } diff --git a/src/sagas/objects.js b/src/sagas/objects.js index 5fac6c3e..5c9f00c2 100644 --- a/src/sagas/objects.js +++ b/src/sagas/objects.js @@ -39,11 +39,12 @@ export const OBJECT_SELECTORS = { function* fetchAndStoreObject(objectType, id, apiCall) { const objectStore = yield select(OBJECT_SELECTORS[objectType]); - if (!objectStore[id]) { - const object = yield apiCall; + let object = objectStore[id]; + if (!object) { + object = yield apiCall; yield put(addToStore(objectType, object)); } - return objectStore[id]; + return object; } function* fetchAndStoreObjects(objectType, apiCall) { diff --git a/src/sagas/topology.js b/src/sagas/topology.js index a15e6a30..815cd842 100644 --- a/src/sagas/topology.js +++ b/src/sagas/topology.js @@ -1,6 +1,15 @@ -import {put} from "redux-saga/effects"; -import {addPropToStoreObject} from "../actions/objects"; -import {fetchLatestDatacenterSucceeded} from "../actions/topology"; +import {call, put, select} from "redux-saga/effects"; +import {addPropToStoreObject, addToStore} from "../actions/objects"; +import { + addTileSucceeded, + cancelNewRoomConstructionSucceeded, + deleteTileSucceeded, + fetchLatestDatacenterSucceeded, + startNewRoomConstructionSucceeded +} from "../actions/topology"; +import {addRoomToDatacenter} from "../api/routes/datacenters"; +import {addTileToRoom, deleteRoom} from "../api/routes/rooms"; +import {deleteTile} from "../api/routes/tiles"; import { fetchAndStoreCoolingItem, fetchAndStoreDatacenter, @@ -27,11 +36,12 @@ export function* onFetchLatestDatacenter(action) { export function* fetchDatacenter(datacenterId) { try { - const datacenter = yield fetchAndStoreDatacenter(datacenterId); - datacenter.roomIds = (yield fetchAndStoreRoomsOfDatacenter(datacenterId)).map(room => room.id); + yield fetchAndStoreDatacenter(datacenterId); + const rooms = yield fetchAndStoreRoomsOfDatacenter(datacenterId); + yield put(addPropToStoreObject("datacenter", datacenterId, {roomIds: rooms.map(room => room.id)})); - for (let index in datacenter.roomIds) { - yield fetchRoom(datacenter.roomIds[index]); + for (let index in rooms) { + yield fetchRoom(rooms[index].id); } } catch (error) { console.log(error); @@ -69,3 +79,53 @@ function* fetchTile(tile) { console.warn("Unknown object type encountered while fetching tile objects"); } } + +export function* onStartNewRoomConstruction() { + try { + const datacenterId = yield select(state => state.currentDatacenterId); + const room = yield call(addRoomToDatacenter, { + id: -1, + datacenterId, + roomType: "SERVER" + }); + const roomWithEmptyTileList = Object.assign(room, {tileIds: []}); + yield put(addToStore("room", roomWithEmptyTileList)); + yield put(startNewRoomConstructionSucceeded(room.id)); + } catch (error) { + console.log(error); + } +} + +export function* onCancelNewRoomConstruction() { + try { + const roomId = yield select(state => state.currentRoomInConstruction); + yield call(deleteRoom, roomId); + yield put(cancelNewRoomConstructionSucceeded()); + } catch (error) { + console.log(error); + } +} + +export function* onAddTile(action) { + try { + const roomId = yield select(state => state.currentRoomInConstruction); + const tile = yield call(addTileToRoom, { + roomId, + positionX: action.positionX, + positionY: action.positionY + }); + yield put(addToStore("tile", tile)); + yield put(addTileSucceeded(tile.id)); + } catch (error) { + console.log(error); + } +} + +export function* onDeleteTile(action) { + try { + yield call(deleteTile, action.tileId); + yield put(deleteTileSucceeded(action.tileId)); + } catch (error) { + console.log(error); + } +} diff --git a/src/util/tile-calculations.js b/src/util/tile-calculations.js index 478c09f9..3050a404 100644 --- a/src/util/tile-calculations.js +++ b/src/util/tile-calculations.js @@ -1,10 +1,10 @@ -export function deriveWallLocations(room) { +export function deriveWallLocations(tiles) { const verticalWalls = {}; const horizontalWalls = {}; // Determine wall segments - room.tiles.forEach(tile => { + tiles.forEach(tile => { const x = tile.positionX, y = tile.positionY; for (let dX = -1; dX <= 1; dX++) { for (let dY = -1; dY <= 1; dY++) { @@ -13,7 +13,7 @@ export function deriveWallLocations(room) { } let doInsert = true; - room.tiles.forEach(otherTile => { + tiles.forEach(otherTile => { if (otherTile.positionX === x + dX && otherTile.positionY === y + dY) { doInsert = false; } |
