diff options
Diffstat (limited to 'opendc-web/opendc-web-server/src/main/webui')
9 files changed, 267 insertions, 27 deletions
diff --git a/opendc-web/opendc-web-server/src/main/webui/api/rack-prefabs.js b/opendc-web/opendc-web-server/src/main/webui/api/rack-prefabs.js new file mode 100644 index 00000000..1792704b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/api/rack-prefabs.js @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { request } from './index' + +export function fetchRackPrefabs(auth, projectId) { + return request(auth, `projects/${projectId}/rack-prefabs`) +} + +export function addRackPrefab(auth, projectId, rackPrefab) { + return request(auth, `projects/${projectId}/rack-prefabs`, 'POST', rackPrefab) +} + +export function deleteRackPrefab(auth, projectId, rackPrefabNumber) { + return request(auth, `projects/${projectId}/rack-prefabs/${rackPrefabNumber}`, 'DELETE') +} diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js index 6a0c3ff3..d3266537 100644 --- a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js +++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/rack/AddPrefab.js @@ -22,15 +22,81 @@ import PropTypes from 'prop-types' import React from 'react' -import { Button } from '@patternfly/react-core' +import { Button, AlertGroup, Alert, AlertVariant, AlertActionCloseButton } from '@patternfly/react-core' import { SaveIcon } from '@patternfly/react-icons' +import { useSelector } from 'react-redux' +import { useRouter } from 'next/router' +import { denormalize } from 'normalizr' +import { Rack } from '../../../../util/topology-schema' +import { useNewRackPrefab } from '../../../../data/rack-prefabs' + +function AddPrefab({ tileId }) { + const [alert, setAlert] = React.useState(null) + const router = useRouter() + const { project: projectId } = router.query + const { mutate: addRackPrefab } = useNewRackPrefab() + + const rackId = useSelector((state) => state.topology.tiles[tileId]?.rack) + const rack = useSelector((state) => { + const topologyState = state.topology + if (!rackId || !topologyState.racks[rackId]) { + return null + } + return denormalize(rackId, Rack, topologyState) + }) + + const onClick = () => { + if (rack && projectId) { + addRackPrefab( + { + projectId, + name: rack.name, + rack: { + ...rack, + id: 0, + machines: rack.machines.map((m) => ({ ...m, id: 0 })), + }, + }, + { + onSuccess: () => { + setAlert({ variant: AlertVariant.success, title: 'Rack saved as prefab' }) + }, + onError: (error) => { + setAlert({ variant: AlertVariant.danger, title: `Failed to save rack: ${error}` }) + }, + } + ) + } + } -function AddPrefab() { - const onClick = () => {} // TODO return ( - <Button variant="primary" icon={<SaveIcon />} isBlock onClick={onClick} className="pf-u-mb-sm"> - Save this rack to a prefab - </Button> + <> + <AlertGroup isToast> + {alert && ( + <Alert + isLiveRegion + variant={alert.variant} + title={alert.title} + actionClose={ + <AlertActionCloseButton + title={alert.title} + onClose={() => setAlert(null)} + /> + } + /> + )} + </AlertGroup> + <Button + variant="primary" + icon={<SaveIcon />} + isBlock + onClick={onClick} + className="pf-u-mb-sm" + isDisabled={!rack} + > + Save this rack to a prefab + </Button> + </> ) } diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js index a384d5d5..f9eab381 100644 --- a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js +++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionComponent.js @@ -1,9 +1,11 @@ import PropTypes from 'prop-types' import React from 'react' -import { Button } from '@patternfly/react-core' +import { Button, FormSelect, FormSelectOption } from '@patternfly/react-core' import { PlusIcon, TimesIcon } from '@patternfly/react-icons' -const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, isEditingRoom }) => { +const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, isEditingRoom, prefabs = [] }) => { + const [selectedPrefabId, setSelectedPrefabId] = React.useState('') + if (inRackConstructionMode) { return ( <Button isBlock={true} icon={<TimesIcon />} onClick={onStop} className="pf-u-mb-sm"> @@ -12,16 +14,38 @@ const RackConstructionComponent = ({ onStart, onStop, inRackConstructionMode, is ) } + const onChangePrefab = (value) => { + setSelectedPrefabId(value) + } + return ( - <Button - icon={<PlusIcon />} - isBlock - isDisabled={isEditingRoom} - onClick={() => (isEditingRoom ? undefined : onStart())} - className="pf-u-mb-sm" - > - Start rack construction - </Button> + <> + <FormSelect + value={selectedPrefabId} + onChange={onChangePrefab} + aria-label="Select rack prefab" + className="pf-u-mb-sm" + > + <FormSelectOption key="" value="" label="Empty Rack" /> + {prefabs.map((prefab) => ( + <FormSelectOption key={prefab.id} value={prefab.id} label={prefab.name} /> + ))} + </FormSelect> + <Button + icon={<PlusIcon />} + isBlock + isDisabled={isEditingRoom} + onClick={() => { + if (!isEditingRoom) { + const prefab = prefabs.find((p) => p.id === parseInt(selectedPrefabId)) + onStart(prefab) + } + }} + className="pf-u-mb-sm" + > + Start rack construction + </Button> + </> ) } @@ -30,6 +54,7 @@ RackConstructionComponent.propTypes = { onStop: PropTypes.func, inRackConstructionMode: PropTypes.bool, isEditingRoom: PropTypes.bool, + prefabs: PropTypes.array, } export default RackConstructionComponent diff --git a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js index e04287a5..70f1b8e6 100644 --- a/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js +++ b/opendc-web/opendc-web-server/src/main/webui/components/topologies/sidebar/room/RackConstructionContainer.js @@ -22,21 +22,28 @@ import React from 'react' import { useDispatch, useSelector } from 'react-redux' +import { useRouter } from 'next/router' import { startRackConstruction, stopRackConstruction } from '../../../../redux/actions/topology/room' +import { useRackPrefabs } from '../../../../data/rack-prefabs' import RackConstructionComponent from './RackConstructionComponent' function RackConstructionContainer(props) { + const router = useRouter() + const { project: projectId } = router.query + const { data: prefabs = [] } = useRackPrefabs(projectId) + const isRackConstructionMode = useSelector((state) => state.construction.inRackConstructionMode) const isEditingRoom = useSelector((state) => state.construction.currentRoomInConstruction !== '-1') const dispatch = useDispatch() - const onStart = () => dispatch(startRackConstruction()) + const onStart = (rackPrefab) => dispatch(startRackConstruction(rackPrefab)) const onStop = () => dispatch(stopRackConstruction()) return ( <RackConstructionComponent {...props} inRackConstructionMode={isRackConstructionMode} isEditingRoom={isEditingRoom} + prefabs={prefabs} onStart={onStart} onStop={onStop} /> diff --git a/opendc-web/opendc-web-server/src/main/webui/data/query.js b/opendc-web/opendc-web-server/src/main/webui/data/query.js index 3e5423b9..109cd2e7 100644 --- a/opendc-web/opendc-web-server/src/main/webui/data/query.js +++ b/opendc-web/opendc-web-server/src/main/webui/data/query.js @@ -25,6 +25,7 @@ import { QueryClient } from 'react-query' import { useAuth } from '../auth' import { configureExperimentClient } from './experiments' import { configureProjectClient } from './project' +import { configureRackPrefabClient } from './rack-prefabs' import { configureTopologyClient } from './topology' import { configureUserClient } from './user' @@ -33,6 +34,7 @@ let queryClient function createQueryClient(auth) { const client = new QueryClient() configureProjectClient(client, auth) + configureRackPrefabClient(client, auth) configureExperimentClient(client, auth) configureTopologyClient(client, auth) configureUserClient(client, auth) diff --git a/opendc-web/opendc-web-server/src/main/webui/data/rack-prefabs.js b/opendc-web/opendc-web-server/src/main/webui/data/rack-prefabs.js new file mode 100644 index 00000000..1979fa24 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/data/rack-prefabs.js @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { useQuery, useMutation } from 'react-query' +import { addRackPrefab, deleteRackPrefab, fetchRackPrefabs } from '../api/rack-prefabs' + +/** + * Configure the query defaults for the rack prefab endpoints. + */ +export function configureRackPrefabClient(queryClient, auth) { + queryClient.setQueryDefaults('rack-prefabs', { + queryFn: ({ queryKey }) => fetchRackPrefabs(auth, queryKey[1]), + }) + + queryClient.setMutationDefaults('addRackPrefab', { + mutationFn: ({ projectId, ...data }) => addRackPrefab(auth, projectId, data), + onSuccess: (result) => { + queryClient.setQueryData(['rack-prefabs', result.project.id], (old = []) => [...old, result]) + }, + }) + queryClient.setMutationDefaults('deleteRackPrefab', { + mutationFn: ({ projectId, number }) => deleteRackPrefab(auth, projectId, number), + onSuccess: (result) => { + queryClient.setQueryData(['rack-prefabs', result.project.id], (old = []) => + old.filter((rackPrefab) => rackPrefab.id !== result.id) + ) + }, + }) +} + +/** + * Fetch all rack prefabs of the specified project. + */ +export function useRackPrefabs(projectId, options = {}) { + return useQuery(['rack-prefabs', projectId], { enabled: !!projectId, ...options }) +} + +/** + * Create a mutation for a new rack prefab. + */ +export function useNewRackPrefab() { + return useMutation('addRackPrefab') +} + +/** + * Create a mutation for deleting a rack prefab. + */ +export function useDeleteRackPrefab(options = {}) { + return useMutation('deleteRackPrefab', options) +} diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/room.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/room.js index 14cc126c..70c93d1f 100644 --- a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/room.js +++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/room.js @@ -1,4 +1,6 @@ import { v4 as uuid } from 'uuid' +import { normalize } from 'normalizr' +import { Rack as RackSchema } from '../../../util/topology-schema' import { DEFAULT_RACK_SLOT_CAPACITY, DEFAULT_RACK_POWER_CAPACITY, @@ -31,9 +33,10 @@ export function editRoomName(roomId, name) { } } -export function startRackConstruction() { +export function startRackConstruction(rackPrefab) { return { type: START_RACK_CONSTRUCTION, + rackPrefab, } } @@ -45,22 +48,34 @@ export function stopRackConstruction() { export function addRackToTile(positionX, positionY) { return (dispatch, getState) => { - const { topology, interactionLevel } = getState() + const { topology, interactionLevel, construction } = getState() const currentRoom = topology.rooms[interactionLevel.roomId] const tiles = currentRoom.tiles.map((tileId) => topology.tiles[tileId]) const tile = findTileWithPosition(tiles, positionX, positionY) if (tile !== null) { + const prefab = construction.currentRackPrefab + const rackId = uuid() + const rack = prefab + ? { + ...prefab.rack, + id: rackId, + machines: (prefab.rack.machines || []).map((m) => ({ ...m, id: uuid(), rackId })), + } + : { + id: rackId, + name: 'Rack', + capacity: DEFAULT_RACK_SLOT_CAPACITY, + powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, + machines: [], + } + + const { entities, result: normalizedRackId } = normalize(rack, RackSchema) dispatch({ type: ADD_RACK_TO_TILE, tileId: tile.id, - rack: { - id: uuid(), - name: 'Rack', - capacity: DEFAULT_RACK_SLOT_CAPACITY, - powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, - machines: [], - }, + rack: entities.racks[normalizedRackId], + entities, }) } } diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js index d0aac5ae..8520e794 100644 --- a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js +++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js @@ -37,7 +37,20 @@ export function inRackConstructionMode(state = false, action) { } } +export function currentRackPrefab(state = null, action) { + switch (action.type) { + case START_RACK_CONSTRUCTION: + return action.rackPrefab || null + case STOP_RACK_CONSTRUCTION: + case GO_DOWN_ONE_INTERACTION_LEVEL: + return null + default: + return state + } +} + export const construction = combineReducers({ currentRoomInConstruction, inRackConstructionMode, + currentRackPrefab, }) diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/machine.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/machine.js index 1789257b..5cf38726 100644 --- a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/machine.js +++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/machine.js @@ -2,11 +2,19 @@ import produce from 'immer' import { STORE_TOPOLOGY } from '../../actions/topology' import { DELETE_MACHINE, ADD_UNIT, DELETE_UNIT } from '../../actions/topology/machine' import { ADD_MACHINE, DELETE_RACK } from '../../actions/topology/rack' +import { ADD_RACK_TO_TILE } from '../../actions/topology/room' function machine(state = {}, action, { racks }) { switch (action.type) { case STORE_TOPOLOGY: return action.entities.machines || {} + case ADD_RACK_TO_TILE: + return produce(state, (draft) => { + const { entities } = action + if (entities && entities.machines) { + Object.assign(draft, entities.machines) + } + }) case ADD_MACHINE: return produce(state, (draft) => { const { machine } = action |
