diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-07-21 15:04:22 +0200 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-07-22 14:23:09 +0200 |
| commit | 54f424a18cc21a52ea518d40893218a07ab55989 (patch) | |
| tree | d754d2705a5daf93178f20ea2a2e39046c6dce8a /opendc-web | |
| parent | 28a4259c43e6180723b15a8c36a9b36871420f8a (diff) | |
feat(ui): Extract topology construction out of Sagas
This change updates the OpenDC frontend to perform the construction of
the topology directly in the reducers instead of performing the
mutations in Redux Sagas as side effects. This allows us to nicely map
actions to mutations in the reducers.
Diffstat (limited to 'opendc-web')
45 files changed, 539 insertions, 614 deletions
diff --git a/opendc-web/opendc-web-ui/package.json b/opendc-web/opendc-web-ui/package.json index 84db4277..5a32806c 100644 --- a/opendc-web/opendc-web-ui/package.json +++ b/opendc-web/opendc-web-ui/package.json @@ -30,6 +30,7 @@ "approximate-number": "~2.0.0", "classnames": "~2.2.5", "husky": "~4.2.5", + "immer": "^9.0.5", "konva": "~7.2.5", "lint-staged": "~10.2.2", "mathjs": "~7.6.0", diff --git a/opendc-web/opendc-web-ui/src/components/topologies/TopologyMap.js b/opendc-web/opendc-web-ui/src/components/topologies/TopologyMap.js index c16f554c..2f27749f 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/TopologyMap.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/TopologyMap.js @@ -39,7 +39,7 @@ import { useSelector } from 'react-redux' import TopologySidebar from './sidebar/TopologySidebar' function TopologyMap() { - const topologyIsLoading = useSelector((state) => state.currentTopologyId === '-1') + const topologyIsLoading = useSelector((state) => !state.topology.root) const interactionLevel = useSelector((state) => state.interactionLevel) const [isExpanded, setExpanded] = useState(true) diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js b/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js index 5d19b3ad..d8735cf1 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js @@ -1,8 +1,8 @@ -import React, { useRef, useState } from 'react' +import React, { useRef, useState, useContext } from 'react' import { HotKeys } from 'react-hotkeys' import { Stage } from 'react-konva' import { MAP_MAX_SCALE, MAP_MIN_SCALE, MAP_MOVE_PIXELS_PER_EVENT, MAP_SCALE_PER_EVENT } from './MapConstants' -import { Provider, useStore } from 'react-redux' +import { ReactReduxContext } from 'react-redux' import useResizeObserver from 'use-resize-observer' import { mapContainer } from './MapStage.module.scss' import MapLayer from './layers/MapLayer' @@ -12,7 +12,7 @@ import ScaleIndicator from './controls/ScaleIndicator' import Toolbar from './controls/Toolbar' function MapStage() { - const store = useStore() + const reduxContext = useContext(ReactReduxContext) const { ref, width = 100, height = 100 } = useResizeObserver() const stageRef = useRef(null) const [[x, y], setPos] = useState([0, 0]) @@ -68,11 +68,11 @@ function MapStage() { x={x} y={y} > - <Provider store={store}> + <ReactReduxContext.Provider value={reduxContext}> <MapLayer /> <RoomHoverLayer /> <ObjectHoverLayer /> - </Provider> + </ReactReduxContext.Provider> </Stage> <ScaleIndicator scale={scale} /> <Toolbar onZoom={onZoomButton} onExport={onExport} /> diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js index c35cbde7..be1f3e45 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RackEnergyFillContainer.js @@ -6,18 +6,18 @@ import RackFillBar from './elements/RackFillBar' function RackSpaceFillContainer({ tileId, ...props }) { const fillFraction = useSelector((state) => { let energyConsumptionTotal = 0 - const rack = state.objects.rack[state.objects.tile[tileId].rack] + const rack = state.topology.racks[state.topology.tiles[tileId].rack] const machineIds = rack.machines machineIds.forEach((machineId) => { if (machineId !== null) { - const machine = state.objects.machine[machineId] - machine.cpus.forEach((id) => (energyConsumptionTotal += state.objects.cpu[id].energyConsumptionW)) - machine.gpus.forEach((id) => (energyConsumptionTotal += state.objects.gpu[id].energyConsumptionW)) + const machine = state.topology.machines[machineId] + machine.cpus.forEach((id) => (energyConsumptionTotal += state.topology.cpus[id].energyConsumptionW)) + machine.gpus.forEach((id) => (energyConsumptionTotal += state.topology.gpus[id].energyConsumptionW)) machine.memories.forEach( - (id) => (energyConsumptionTotal += state.objects.memory[id].energyConsumptionW) + (id) => (energyConsumptionTotal += state.topology.memories[id].energyConsumptionW) ) machine.storages.forEach( - (id) => (energyConsumptionTotal += state.objects.storage[id].energyConsumptionW) + (id) => (energyConsumptionTotal += state.topology.storages[id].energyConsumptionW) ) } }) diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.js index a6766f33..0c15d54b 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RackSpaceFillContainer.js @@ -26,7 +26,7 @@ import { useSelector } from 'react-redux' import RackFillBar from './elements/RackFillBar' function RackSpaceFillContainer({ tileId, ...props }) { - const rack = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack]) + const rack = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack]) return <RackFillBar {...props} type="space" fillFraction={rack.machines.length / rack.capacity} /> } diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js index 93ba9c93..65189891 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/RoomContainer.js @@ -31,7 +31,7 @@ function RoomContainer({ roomId, ...props }) { return { interactionLevel: state.interactionLevel, currentRoomInConstruction: state.construction.currentRoomInConstruction, - room: state.objects.room[roomId], + room: state.topology.rooms[roomId], } }) const dispatch = useDispatch() diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js index 149e26a1..411a5ca7 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/TileContainer.js @@ -28,7 +28,7 @@ import TileGroup from './groups/TileGroup' function TileContainer({ tileId, ...props }) { const interactionLevel = useSelector((state) => state.interactionLevel) - const tile = useSelector((state) => state.objects.tile[tileId]) + const tile = useSelector((state) => state.topology.tiles[tileId]) const dispatch = useDispatch() const onClick = (tile) => { diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/TopologyContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/TopologyContainer.js index eaebabd5..cc0d46b3 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/TopologyContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/TopologyContainer.js @@ -22,11 +22,10 @@ import React from 'react' import { useSelector } from 'react-redux' -import { useActiveTopology } from '../../../data/topology' import TopologyGroup from './groups/TopologyGroup' function TopologyContainer() { - const topology = useActiveTopology() + const topology = useSelector((state) => state.topology.root) const interactionLevel = useSelector((state) => state.interactionLevel) return <TopologyGroup topology={topology} interactionLevel={interactionLevel} /> diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.js index 77f553dd..143f70c2 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/WallContainer.js @@ -26,7 +26,9 @@ import { useSelector } from 'react-redux' import WallGroup from './groups/WallGroup' function WallContainer({ roomId, ...props }) { - const tiles = useSelector((state) => state.objects.room[roomId].tiles.map((tileId) => state.objects.tile[tileId])) + const tiles = useSelector((state) => { + return state.topology.rooms[roomId].tiles.map((tileId) => state.topology.tiles[tileId]) + }) return <WallGroup {...props} tiles={tiles} /> } diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/ObjectHoverLayer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/ObjectHoverLayer.js index 47d9c992..1f00de36 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/ObjectHoverLayer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/ObjectHoverLayer.js @@ -34,8 +34,8 @@ function ObjectHoverLayer() { return false } - const currentRoom = state.objects.room[state.interactionLevel.roomId] - const tiles = currentRoom.tiles.map((tileId) => state.objects.tile[tileId]) + const currentRoom = state.topology.rooms[state.interactionLevel.roomId] + const tiles = currentRoom.tiles.map((tileId) => state.topology.tiles[tileId]) const tile = findTileWithPosition(tiles, x, y) return !(tile === null || tile.rack) diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js index 59f83b2b..5e351691 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js @@ -35,17 +35,17 @@ function RoomHoverLayer() { const onClick = (x, y) => dispatch(toggleTileAtLocation(x, y)) const isEnabled = useSelector((state) => state.construction.currentRoomInConstruction !== '-1') const isValid = useSelector((state) => (x, y) => { - const newRoom = { ...state.objects.room[state.construction.currentRoomInConstruction] } - const oldRooms = Object.keys(state.objects.room) - .map((id) => ({ ...state.objects.room[id] })) + const newRoom = { ...state.topology.rooms[state.construction.currentRoomInConstruction] } + const oldRooms = Object.keys(state.topology.rooms) + .map((id) => ({ ...state.topology.rooms[id] })) .filter( (room) => - state.objects.topology[state.currentTopologyId].rooms.indexOf(room._id) !== -1 && + state.topology.root.rooms.indexOf(room._id) !== -1 && room._id !== state.construction.currentRoomInConstruction ) ;[...oldRooms, newRoom].forEach((room) => { - room.tiles = room.tiles.map((tileId) => state.objects.tile[tileId]) + room.tiles = room.tiles.map((tileId) => state.topology.tiles[tileId]) }) if (newRoom.tiles.length === 0) { return findPositionInRooms(oldRooms, x, y) === -1 diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/DeleteMachine.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/DeleteMachine.js index 00ce4603..a4b9457b 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/DeleteMachine.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/DeleteMachine.js @@ -20,21 +20,20 @@ * SOFTWARE. */ +import PropTypes from 'prop-types' import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import { Button } from '@patternfly/react-core' import { TrashIcon } from '@patternfly/react-icons' import ConfirmationModal from '../../../util/modals/ConfirmationModal' import { deleteMachine } from '../../../../redux/actions/topology/machine' -function DeleteMachine() { +function DeleteMachine({ machineId }) { const dispatch = useDispatch() const [isVisible, setVisible] = useState(false) - const rackId = useSelector((state) => state.objects.tile[state.interactionLevel.tileId].rack) - const position = useSelector((state) => state.interactionLevel.position) const callback = (isConfirmed) => { if (isConfirmed) { - dispatch(deleteMachine(rackId, position)) + dispatch(deleteMachine(machineId)) } setVisible(false) } @@ -53,4 +52,8 @@ function DeleteMachine() { ) } +DeleteMachine.propTypes = { + machineId: PropTypes.string.isRequired, +} + export default DeleteMachine diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js index 0c3dea98..9268f615 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js @@ -13,9 +13,9 @@ import { import { useSelector } from 'react-redux' function MachineSidebar({ tileId, position }) { - const machine = useSelector(({ objects }) => { - const rack = objects.rack[objects.tile[tileId].rack] - return objects.machine[rack.machines[position - 1]] + const machine = useSelector(({ topology }) => { + const rack = topology.racks[topology.tiles[tileId].rack] + return topology.machines[rack.machines[position - 1]] }) const machineId = machine._id return ( @@ -30,7 +30,7 @@ function MachineSidebar({ tileId, position }) { </TextList> <Title headingLevel="h2">Actions</Title> - <DeleteMachine /> + <DeleteMachine machineId={machineId} /> <Title headingLevel="h2">Units</Title> </TextContent> diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddContainer.js index fc805b95..6b136120 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddContainer.js @@ -27,7 +27,7 @@ import UnitAddComponent from './UnitAddComponent' import { addUnit } from '../../../../redux/actions/topology/machine' function UnitAddContainer({ machineId, unitType }) { - const units = useSelector((state) => Object.values(state.objects[unitType])) + const units = useSelector((state) => Object.values(state.topology[unitType])) const dispatch = useDispatch() const onAdd = (id) => dispatch(addUnit(machineId, unitType, id)) diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js index 901fa45b..6dcc414f 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js @@ -26,18 +26,11 @@ import { useDispatch, useSelector } from 'react-redux' import UnitListComponent from './UnitListComponent' import { deleteUnit } from '../../../../redux/actions/topology/machine' -const unitMapping = { - cpu: 'cpus', - gpu: 'gpus', - memory: 'memories', - storage: 'storages', -} - function UnitListContainer({ machineId, unitType }) { const dispatch = useDispatch() const units = useSelector((state) => { - const machine = state.objects.machine[machineId] - return machine[unitMapping[unitType]].map((id) => state.objects[unitType][id]) + const machine = state.topology.machines[machineId] + return machine[unitType].map((id) => state.topology[unitType][id]) }) const onDelete = (unit) => dispatch(deleteUnit(machineId, unitType, unit._id)) diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitTabsComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitTabsComponent.js index 6d10d2df..b800e9d4 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitTabsComponent.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitTabsComponent.js @@ -10,20 +10,20 @@ function UnitTabsComponent({ machineId }) { return ( <Tabs activeKey={activeTab} onSelect={(_, tab) => setActiveTab(tab)}> <Tab eventKey="cpu-units" title={<TabTitleText>CPU</TabTitleText>}> - <UnitAddContainer machineId={machineId} unitType="cpu" /> - <UnitListContainer machineId={machineId} unitType="cpu" /> + <UnitAddContainer machineId={machineId} unitType="cpus" /> + <UnitListContainer machineId={machineId} unitType="cpus" /> </Tab> <Tab eventKey="gpu-units" title={<TabTitleText>GPU</TabTitleText>}> - <UnitAddContainer machineId={machineId} unitType="gpu" /> - <UnitListContainer machineId={machineId} unitType="gpu" /> + <UnitAddContainer machineId={machineId} unitType="gpus" /> + <UnitListContainer machineId={machineId} unitType="gpus" /> </Tab> <Tab eventKey="memory-units" title={<TabTitleText>Memory</TabTitleText>}> - <UnitAddContainer machineId={machineId} unitType="memory" /> - <UnitListContainer machineId={machineId} unitType="memory" /> + <UnitAddContainer machineId={machineId} unitType="memories" /> + <UnitListContainer machineId={machineId} unitType="memories" /> </Tab> <Tab eventKey="storage-units" title={<TabTitleText>Storage</TabTitleText>}> - <UnitAddContainer machineId={machineId} unitType="storage" /> - <UnitListContainer machineId={machineId} unitType="storage" /> + <UnitAddContainer machineId={machineId} unitType="storages" /> + <UnitListContainer machineId={machineId} unitType="storages" /> </Tab> </Tabs> ) diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/DeleteRackContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/DeleteRackContainer.js index 80c6349a..0583a7a4 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/DeleteRackContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/DeleteRackContainer.js @@ -22,7 +22,7 @@ import PropTypes from 'prop-types' import React, { useState } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon' import { Button } from '@patternfly/react-core' import ConfirmationModal from '../../../util/modals/ConfirmationModal' @@ -31,9 +31,10 @@ import { deleteRack } from '../../../../redux/actions/topology/rack' function DeleteRackContainer({ tileId }) { const dispatch = useDispatch() const [isVisible, setVisible] = useState(false) + const rackId = useSelector((state) => state.topology.tiles[tileId].rack) const callback = (isConfirmed) => { if (isConfirmed) { - dispatch(deleteRack(tileId)) + dispatch(deleteRack(tileId, rackId)) } setVisible(false) } diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js index 6fbff949..619bb4e2 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListContainer.js @@ -28,8 +28,8 @@ import { goFromRackToMachine } from '../../../../redux/actions/interaction-level import { addMachine } from '../../../../redux/actions/topology/rack' function MachineListContainer({ tileId, ...props }) { - const rack = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack]) - const machines = useSelector((state) => rack.machines.map((id) => state.objects.machine[id])) + const rack = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack]) + const machines = useSelector((state) => rack.machines.map((id) => state.topology.machines[id])) const machinesNull = useMemo(() => { const res = Array(rack.capacity).fill(null) for (const machine of machines) { diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js index 09d73af7..30f38cce 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackNameContainer.js @@ -5,7 +5,7 @@ import NameComponent from '../NameComponent' import { editRackName } from '../../../../redux/actions/topology/rack' const RackNameContainer = ({ tileId }) => { - const { name: rackName, _id } = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack]) + const { name: rackName, _id } = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack]) const dispatch = useDispatch() const callback = (name) => { if (name) { diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackSidebar.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackSidebar.js index 3c9f152a..8f6ff135 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackSidebar.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/RackSidebar.js @@ -17,7 +17,7 @@ import { import { useSelector } from 'react-redux' function RackSidebar({ tileId }) { - const rack = useSelector((state) => state.objects.rack[state.objects.tile[tileId].rack]) + const rack = useSelector((state) => state.topology.racks[state.topology.tiles[tileId].rack]) return ( <div className={sidebarContainer}> diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js index e8d8b33c..fb52d826 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/room/RoomName.js @@ -27,7 +27,7 @@ import NameComponent from '../NameComponent' import { editRoomName } from '../../../../redux/actions/topology/room' function RoomName({ roomId }) { - const { name: roomName, _id } = useSelector((state) => state.objects.room[roomId]) + const { name: roomName, _id } = useSelector((state) => state.topology.rooms[roomId]) const dispatch = useDispatch() const callback = (name) => { if (name) { diff --git a/opendc-web/opendc-web-ui/src/data/topology.js b/opendc-web/opendc-web-ui/src/data/topology.js index 83abb6aa..e068ed8e 100644 --- a/opendc-web/opendc-web-ui/src/data/topology.js +++ b/opendc-web/opendc-web-ui/src/data/topology.js @@ -20,7 +20,6 @@ * SOFTWARE. */ -import { useSelector } from 'react-redux' import { useQuery } from 'react-query' import { addTopology, deleteTopology, fetchTopologiesOfProject, fetchTopology, updateTopology } from '../api/topologies' @@ -66,13 +65,6 @@ export function configureTopologyClient(queryClient, auth) { /** * Return the current active topology. */ -export function useActiveTopology() { - return useSelector((state) => state.currentTopologyId !== '-1' && state.objects.topology[state.currentTopologyId]) -} - -/** - * Return the current active topology. - */ export function useTopology(topologyId, options = {}) { return useQuery(['topologies', topologyId], { enabled: !!topologyId, ...options }) } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/objects.js b/opendc-web/opendc-web-ui/src/redux/actions/objects.js deleted file mode 100644 index 7b648b18..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/objects.js +++ /dev/null @@ -1,41 +0,0 @@ -export const ADD_TO_STORE = 'ADD_TO_STORE' -export const ADD_PROP_TO_STORE_OBJECT = 'ADD_PROP_TO_STORE_OBJECT' -export const ADD_ID_TO_STORE_OBJECT_LIST_PROP = 'ADD_ID_TO_STORE_OBJECT_LIST_PROP' -export const REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP = 'REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP' - -export function addToStore(objectType, object) { - return { - type: ADD_TO_STORE, - objectType, - object, - } -} - -export function addPropToStoreObject(objectType, objectId, propObject) { - return { - type: ADD_PROP_TO_STORE_OBJECT, - objectType, - objectId, - propObject, - } -} - -export function addIdToStoreObjectListProp(objectType, objectId, propName, id) { - return { - type: ADD_ID_TO_STORE_OBJECT_LIST_PROP, - objectType, - objectId, - propName, - id, - } -} - -export function removeIdFromStoreObjectListProp(objectType, objectId, propName, id) { - return { - type: REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP, - objectType, - objectId, - propName, - id, - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js b/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js deleted file mode 100644 index 0ef7795f..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/prefabs.js +++ /dev/null @@ -1,33 +0,0 @@ -export const ADD_PREFAB = 'ADD_PREFAB' -export const DELETE_PREFAB = 'DELETE_PREFAB' -export const DELETE_PREFAB_SUCCEEDED = 'DELETE_PREFAB_SUCCEEDED' -export const OPEN_PREFAB_SUCCEEDED = 'OPEN_PREFAB_SUCCEEDED' - -export function addPrefab(name, tileId) { - return { - type: ADD_PREFAB, - name, - tileId, - } -} - -export function deletePrefab(id) { - return { - type: DELETE_PREFAB, - id, - } -} - -export function deletePrefabSucceeded(id) { - return { - type: DELETE_PREFAB_SUCCEEDED, - id, - } -} - -export function openPrefabSucceeded(id) { - return { - type: OPEN_PREFAB_SUCCEEDED, - id, - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topologies.js b/opendc-web/opendc-web-ui/src/redux/actions/topologies.js index 4888c4da..fc697cc2 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topologies.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topologies.js @@ -18,9 +18,10 @@ export function addTopology(projectId, name, duplicateId) { } } -export function storeTopology(entities) { +export function storeTopology(topology, entities) { return { type: STORE_TOPOLOGY, + topology, entities, } } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js index 49425318..939c24a4 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/building.js @@ -1,4 +1,6 @@ -export const SET_CURRENT_TOPOLOGY = 'SET_CURRENT_TOPOLOGY' +import { uuid } from 'uuidv4' +import { addRoom, deleteRoom } from './room' + export const START_NEW_ROOM_CONSTRUCTION = 'START_NEW_ROOM_CONSTRUCTION' export const START_NEW_ROOM_CONSTRUCTION_SUCCEEDED = 'START_NEW_ROOM_CONSTRUCTION_SUCCEEDED' export const FINISH_NEW_ROOM_CONSTRUCTION = 'FINISH_NEW_ROOM_CONSTRUCTION' @@ -9,16 +11,19 @@ export const FINISH_ROOM_EDIT = 'FINISH_ROOM_EDIT' export const ADD_TILE = 'ADD_TILE' export const DELETE_TILE = 'DELETE_TILE' -export function setCurrentTopology(topologyId) { - return { - type: SET_CURRENT_TOPOLOGY, - topologyId, - } -} - export function startNewRoomConstruction() { - return { - type: START_NEW_ROOM_CONSTRUCTION, + return (dispatch, getState) => { + const { topology } = getState() + const topologyId = topology.root._id + const room = { + _id: uuid(), + name: 'Room', + topologyId, + tiles: [], + } + + dispatch(addRoom(topologyId, room)) + dispatch(startNewRoomConstructionSucceeded(room._id)) } } @@ -31,8 +36,8 @@ export function startNewRoomConstructionSucceeded(roomId) { export function finishNewRoomConstruction() { return (dispatch, getState) => { - const { objects, construction } = getState() - if (objects.room[construction.currentRoomInConstruction].tiles.length === 0) { + const { topology, construction } = getState() + if (topology.rooms[construction.currentRoomInConstruction].tiles.length === 0) { dispatch(cancelNewRoomConstruction()) return } @@ -44,8 +49,11 @@ export function finishNewRoomConstruction() { } export function cancelNewRoomConstruction() { - return { - type: CANCEL_NEW_ROOM_CONSTRUCTION, + return (dispatch, getState) => { + const { construction } = getState() + const roomId = construction.currentRoomInConstruction + dispatch(deleteRoom(roomId)) + dispatch(cancelNewRoomConstructionSucceeded()) } } @@ -70,24 +78,30 @@ export function finishRoomEdit() { export function toggleTileAtLocation(positionX, positionY) { return (dispatch, getState) => { - const { objects, construction } = getState() + const { topology, construction } = getState() - const tileIds = objects.room[construction.currentRoomInConstruction].tiles + const roomId = construction.currentRoomInConstruction + const tileIds = topology.rooms[roomId].tiles for (const tileId of tileIds) { - if (objects.tile[tileId].positionX === positionX && objects.tile[tileId].positionY === positionY) { + if (topology.tiles[tileId].positionX === positionX && topology.tiles[tileId].positionY === positionY) { dispatch(deleteTile(tileId)) return } } - dispatch(addTile(positionX, positionY)) + + dispatch(addTile(roomId, positionX, positionY)) } } -export function addTile(positionX, positionY) { +export function addTile(roomId, positionX, positionY) { return { type: ADD_TILE, - positionX, - positionY, + tile: { + _id: uuid(), + roomId, + positionX, + positionY, + }, } } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js index 170b7648..93320884 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/machine.js @@ -2,28 +2,27 @@ export const DELETE_MACHINE = 'DELETE_MACHINE' export const ADD_UNIT = 'ADD_UNIT' export const DELETE_UNIT = 'DELETE_UNIT' -export function deleteMachine(rackId, position) { +export function deleteMachine(machineId) { return { type: DELETE_MACHINE, - rackId, - position, + machineId, } } -export function addUnit(machineId, unitType, id) { +export function addUnit(machineId, unitType, unitId) { return { type: ADD_UNIT, machineId, unitType, - id, + unitId, } } -export function deleteUnit(machineId, unitType, index) { +export function deleteUnit(machineId, unitType, unitId) { return { type: DELETE_UNIT, machineId, unitType, - index, + unitId, } } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js index 228e3ae9..c319d966 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/rack.js @@ -1,3 +1,5 @@ +import { uuid } from 'uuidv4' + export const EDIT_RACK_NAME = 'EDIT_RACK_NAME' export const DELETE_RACK = 'DELETE_RACK' export const ADD_MACHINE = 'ADD_MACHINE' @@ -10,9 +12,10 @@ export function editRackName(rackId, name) { } } -export function deleteRack(tileId) { +export function deleteRack(tileId, rackId) { return { type: DELETE_RACK, + rackId, tileId, } } @@ -20,7 +23,14 @@ export function deleteRack(tileId) { export function addMachine(rackId, position) { return { type: ADD_MACHINE, - position, - rackId, + machine: { + _id: uuid(), + rackId, + position, + cpus: [], + gpus: [], + memories: [], + storages: [], + }, } } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js b/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js index e584af89..bd447db5 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/topology/room.js @@ -1,11 +1,28 @@ +import { uuid } from 'uuidv4' +import { + DEFAULT_RACK_SLOT_CAPACITY, + DEFAULT_RACK_POWER_CAPACITY, +} from '../../../components/topologies/map/MapConstants' import { findTileWithPosition } from '../../../util/tile-calculations' +export const ADD_ROOM = 'ADD_ROOM' export const EDIT_ROOM_NAME = 'EDIT_ROOM_NAME' export const DELETE_ROOM = 'DELETE_ROOM' export const START_RACK_CONSTRUCTION = 'START_RACK_CONSTRUCTION' export const STOP_RACK_CONSTRUCTION = 'STOP_RACK_CONSTRUCTION' export const ADD_RACK_TO_TILE = 'ADD_RACK_TO_TILE' +export function addRoom(topologyId, room) { + return { + type: ADD_ROOM, + room: { + _id: uuid(), + topologyId, + ...room, + }, + } +} + export function editRoomName(roomId, name) { return { type: EDIT_ROOM_NAME, @@ -28,15 +45,22 @@ export function stopRackConstruction() { export function addRackToTile(positionX, positionY) { return (dispatch, getState) => { - const { objects, interactionLevel } = getState() - const currentRoom = objects.room[interactionLevel.roomId] - const tiles = currentRoom.tiles.map((tileId) => objects.tile[tileId]) + const { topology, interactionLevel } = getState() + const currentRoom = topology.rooms[interactionLevel.roomId] + const tiles = currentRoom.tiles.map((tileId) => topology.tiles[tileId]) const tile = findTileWithPosition(tiles, positionX, positionY) if (tile !== null) { dispatch({ type: ADD_RACK_TO_TILE, - tileId: tile._id, + rack: { + _id: uuid(), + name: 'Rack', + tileId: tile._id, + capacity: DEFAULT_RACK_SLOT_CAPACITY, + powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, + machines: [], + }, }) } } diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js b/opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js index 5bac7fea..d0aac5ae 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/construction-mode.js @@ -4,7 +4,6 @@ import { CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED, FINISH_NEW_ROOM_CONSTRUCTION, FINISH_ROOM_EDIT, - SET_CURRENT_TOPOLOGY, START_NEW_ROOM_CONSTRUCTION_SUCCEEDED, START_ROOM_EDIT, } from '../actions/topology/building' @@ -19,7 +18,6 @@ export function currentRoomInConstruction(state = '-1', action) { case CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED: case FINISH_NEW_ROOM_CONSTRUCTION: case FINISH_ROOM_EDIT: - case SET_CURRENT_TOPOLOGY: case DELETE_ROOM: return '-1' default: @@ -32,7 +30,6 @@ export function inRackConstructionMode(state = false, action) { case START_RACK_CONSTRUCTION: return true case STOP_RACK_CONSTRUCTION: - case SET_CURRENT_TOPOLOGY: case GO_DOWN_ONE_INTERACTION_LEVEL: return false default: diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js b/opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js deleted file mode 100644 index c0baf567..00000000 --- a/opendc-web/opendc-web-ui/src/redux/reducers/current-ids.js +++ /dev/null @@ -1,10 +0,0 @@ -import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building' - -export function currentTopologyId(state = '-1', action) { - switch (action.type) { - case SET_CURRENT_TOPOLOGY: - return action.topologyId - default: - return state - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/index.js b/opendc-web/opendc-web-ui/src/redux/reducers/index.js index 2f1359d6..7ffb1211 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/index.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/index.js @@ -1,13 +1,11 @@ import { combineReducers } from 'redux' import { construction } from './construction-mode' -import { currentTopologyId } from './current-ids' import { interactionLevel } from './interaction-level' -import { objects } from './objects' +import topology from './topology' const rootReducer = combineReducers({ - objects, + topology, construction, - currentTopologyId, interactionLevel, }) diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js b/opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js index 9f23949f..b30c68b9 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/interaction-level.js @@ -4,14 +4,12 @@ import { GO_FROM_RACK_TO_MACHINE, GO_FROM_ROOM_TO_RACK, } from '../actions/interaction-level' -import { SET_CURRENT_TOPOLOGY } from '../actions/topology/building' +import { DELETE_MACHINE } from '../actions/topology/machine' +import { DELETE_RACK } from '../actions/topology/rack' +import { DELETE_ROOM } from '../actions/topology/room' export function interactionLevel(state = { mode: 'BUILDING' }, action) { switch (action.type) { - case SET_CURRENT_TOPOLOGY: - return { - mode: 'BUILDING', - } case GO_FROM_BUILDING_TO_ROOM: return { mode: 'ROOM', @@ -49,6 +47,21 @@ export function interactionLevel(state = { mode: 'BUILDING' }, action) { } else { return state } + case DELETE_MACHINE: + return { + mode: 'RACK', + roomId: state.roomId, + tileId: state.tileId, + } + case DELETE_RACK: + return { + mode: 'ROOM', + roomId: state.roomId, + } + case DELETE_ROOM: + return { + mode: 'BUILDING', + } default: return state } diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/objects.js b/opendc-web/opendc-web-ui/src/redux/reducers/objects.js deleted file mode 100644 index 11f6d353..00000000 --- a/opendc-web/opendc-web-ui/src/redux/reducers/objects.js +++ /dev/null @@ -1,56 +0,0 @@ -import { combineReducers } from 'redux' -import { - ADD_ID_TO_STORE_OBJECT_LIST_PROP, - ADD_PROP_TO_STORE_OBJECT, - ADD_TO_STORE, - REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP, -} from '../actions/objects' -import { CPU_UNITS, GPU_UNITS, MEMORY_UNITS, STORAGE_UNITS } from '../../util/unit-specifications' -import { STORE_TOPOLOGY } from '../actions/topologies' - -export const objects = combineReducers({ - cpu: object('cpu', CPU_UNITS), - gpu: object('gpu', GPU_UNITS), - memory: object('memory', MEMORY_UNITS), - storage: object('storage', STORAGE_UNITS), - machine: object('machine'), - rack: object('rack'), - tile: object('tile'), - room: object('room'), - topology: object('topology'), - prefab: object('prefab'), -}) - -function object(type, defaultState = {}) { - return objectWithId(type, (object) => object._id, defaultState) -} - -function objectWithId(type, getId, defaultState = {}) { - return (state = defaultState, action) => { - if (action.type === STORE_TOPOLOGY) { - return { ...state, ...action.entities[type] } - } else if (action.objectType !== type) { - return state - } - - if (action.type === ADD_TO_STORE) { - return { ...state, [getId(action.object)]: action.object } - } else if (action.type === ADD_PROP_TO_STORE_OBJECT) { - return { ...state, [action.objectId]: { ...state[action.objectId], ...action.propObject } } - } else if (action.type === ADD_ID_TO_STORE_OBJECT_LIST_PROP) { - return Object.assign({}, state, { - [action.objectId]: Object.assign({}, state[action.objectId], { - [action.propName]: [...state[action.objectId][action.propName], action.id], - }), - }) - } else if (action.type === REMOVE_ID_FROM_STORE_OBJECT_LIST_PROP) { - return Object.assign({}, state, { - [action.objectId]: Object.assign({}, state[action.objectId], { - [action.propName]: state[action.objectId][action.propName].filter((id) => id !== action.id), - }), - }) - } - - return state - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/query.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/index.js index 787006c7..b1c7d29e 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/query.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/index.js @@ -20,22 +20,25 @@ * SOFTWARE. */ -import { MutationObserver } from 'react-query' -import { getContext, call } from 'redux-saga/effects' +import { CPU_UNITS, GPU_UNITS, MEMORY_UNITS, STORAGE_UNITS } from '../../../util/unit-specifications' +import machine from './machine' +import rack from './rack' +import room from './room' +import tile from './tile' +import topology from './topology' -/** - * Fetch the query with the specified key. - */ -export function* fetchQuery(key, options) { - const queryClient = yield getContext('queryClient') - return yield call([queryClient, queryClient.fetchQuery], key, options) +function objects(state = {}, action) { + return { + cpus: CPU_UNITS, + gpus: GPU_UNITS, + memories: MEMORY_UNITS, + storages: STORAGE_UNITS, + machines: machine(state.machines, action, state), + racks: rack(state.racks, action, state), + tiles: tile(state.tiles, action, state), + rooms: room(state.rooms, action, state), + root: topology(state.root, action, state), + } } -/** - * Perform a mutation with the specified key. - */ -export function* mutate(key, object, options) { - const queryClient = yield getContext('queryClient') - const mutationObserver = new MutationObserver(queryClient, { mutationKey: key }) - return yield call([mutationObserver, mutationObserver.mutate], object, options) -} +export default objects diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js new file mode 100644 index 00000000..41773014 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/machine.js @@ -0,0 +1,47 @@ +import produce from 'immer' +import { STORE_TOPOLOGY } from '../../actions/topologies' +import { DELETE_MACHINE, ADD_UNIT, DELETE_UNIT } from '../../actions/topology/machine' +import { ADD_MACHINE, DELETE_RACK } from '../../actions/topology/rack' + +function machine(state = {}, action, { racks }) { + switch (action.type) { + case STORE_TOPOLOGY: + return action.entities.machines || {} + case ADD_MACHINE: + return produce(state, (draft) => { + const { machine } = action + draft[machine._id] = machine + }) + case DELETE_MACHINE: + return produce(state, (draft) => { + const { machineId } = action + delete draft[machineId] + }) + case ADD_UNIT: + return produce(state, (draft) => { + const { machineId, unitType, unitId } = action + draft[machineId][unitType].push(unitId) + }) + case DELETE_UNIT: + return produce(state, (draft) => { + const { machineId, unitType, unitId } = action + const units = draft[machineId][unitType] + const index = units.indexOf(unitId) + units.splice(index, 1) + }) + case DELETE_RACK: + return produce(state, (draft) => { + const { rackId } = action + const rack = racks[rackId] + + for (const id of rack.machines) { + const machine = draft[id] + machine.rackId = undefined + } + }) + default: + return state + } +} + +export default machine diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js new file mode 100644 index 00000000..9cc37124 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/rack.js @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import produce from 'immer' +import { STORE_TOPOLOGY } from '../../actions/topologies' +import { DELETE_MACHINE } from '../../actions/topology/machine' +import { DELETE_RACK, EDIT_RACK_NAME, ADD_MACHINE } from '../../actions/topology/rack' +import { ADD_RACK_TO_TILE } from '../../actions/topology/room' + +function rack(state = {}, action, { machines }) { + switch (action.type) { + case STORE_TOPOLOGY: + return action.entities.racks || {} + case ADD_RACK_TO_TILE: + return produce(state, (draft) => { + const { rack } = action + draft[rack._id] = rack + }) + case EDIT_RACK_NAME: + return produce(state, (draft) => { + const { rackId, name } = action + draft[rackId].name = name + }) + case DELETE_RACK: + return produce(state, (draft) => { + const { rackId } = action + delete draft[rackId] + }) + case ADD_MACHINE: + return produce(state, (draft) => { + const { machine } = action + draft[machine.rackId].machines.push(machine._id) + }) + case DELETE_MACHINE: + return produce(state, (draft) => { + const { machineId } = action + const machine = machines[machineId] + const rack = draft[machine.rackId] + const index = rack.machines.indexOf(machineId) + rack.machines.splice(index, 1) + }) + default: + return state + } +} + +export default rack diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js new file mode 100644 index 00000000..b61c9d82 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/room.js @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import produce from 'immer' +import { STORE_TOPOLOGY } from '../../actions/topologies' +import { ADD_TILE, DELETE_TILE } from '../../actions/topology/building' +import { DELETE_ROOM, EDIT_ROOM_NAME, ADD_ROOM } from '../../actions/topology/room' + +function room(state = {}, action, { tiles }) { + switch (action.type) { + case STORE_TOPOLOGY: + return action.entities.rooms || {} + case ADD_ROOM: + return produce(state, (draft) => { + const { room } = action + draft[room._id] = room + }) + case DELETE_ROOM: + return produce(state, (draft) => { + const { roomId } = action + delete draft[roomId] + }) + case EDIT_ROOM_NAME: + return produce(state, (draft) => { + const { roomId, name } = action + draft[roomId].name = name + }) + case ADD_TILE: + return produce(state, (draft) => { + const { tile } = action + draft[tile.roomId].tiles.push(tile._id) + }) + case DELETE_TILE: + return produce(state, (draft) => { + const { tileId } = action + const tile = tiles[tileId] + const room = draft[tile.roomId] + const index = room.tiles.indexOf(tileId) + room.tiles.splice(index, 1) + }) + default: + return state + } +} + +export default room diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js new file mode 100644 index 00000000..e0c5dd33 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import produce from 'immer' +import { STORE_TOPOLOGY } from '../../actions/topologies' +import { ADD_TILE, DELETE_TILE } from '../../actions/topology/building' +import { DELETE_RACK } from '../../actions/topology/rack' +import { ADD_RACK_TO_TILE } from '../../actions/topology/room' + +function tile(state = {}, action, { racks }) { + switch (action.type) { + case STORE_TOPOLOGY: + return action.entities.tiles || {} + case ADD_TILE: + return produce(state, (draft) => { + const { tile } = action + draft[tile._id] = tile + }) + case DELETE_TILE: + return produce(state, (draft) => { + const { tileId } = action + delete draft[tileId] + }) + case ADD_RACK_TO_TILE: + return produce(state, (draft) => { + const { rack } = action + draft[rack.tileId].rack = rack._id + }) + case DELETE_RACK: + return produce(state, (draft) => { + const { rackId } = action + const rack = racks[rackId] + draft[rack.tileId].rack = undefined + }) + default: + return state + } +} + +export default tile diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js new file mode 100644 index 00000000..da0e6988 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/topology.js @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import produce from 'immer' +import { STORE_TOPOLOGY } from '../../actions/topologies' +import { ADD_ROOM, DELETE_ROOM } from '../../actions/topology/room' + +function topology(state = undefined, action) { + switch (action.type) { + case STORE_TOPOLOGY: + return action.topology + case ADD_ROOM: + return produce(state, (draft) => { + const { room } = action + draft.rooms.push(room._id) + }) + case DELETE_ROOM: + return produce(state, (draft) => { + const { roomId } = action + const index = draft.rooms.indexOf(roomId) + draft.rooms.splice(index, 1) + }) + default: + return state + } +} + +export default topology diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/index.js b/opendc-web/opendc-web-ui/src/redux/sagas/index.js index 9ddc564d..0fabdb6d 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/index.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/index.js @@ -1,51 +1,7 @@ -import { takeEvery } from 'redux-saga/effects' -import { - ADD_TILE, - CANCEL_NEW_ROOM_CONSTRUCTION, - DELETE_TILE, - START_NEW_ROOM_CONSTRUCTION, -} from '../actions/topology/building' -import { ADD_UNIT, DELETE_MACHINE, DELETE_UNIT } from '../actions/topology/machine' -import { ADD_MACHINE, DELETE_RACK, EDIT_RACK_NAME } from '../actions/topology/rack' -import { ADD_RACK_TO_TILE, DELETE_ROOM, EDIT_ROOM_NAME } from '../actions/topology/room' -import { - onAddMachine, - onAddRackToTile, - onAddTile, - onAddTopology, - onAddUnit, - onCancelNewRoomConstruction, - onDeleteMachine, - onDeleteRack, - onDeleteRoom, - onDeleteTile, - onDeleteUnit, - onEditRackName, - onEditRoomName, - onStartNewRoomConstruction, - onOpenTopology, -} from './topology' -import { ADD_TOPOLOGY, OPEN_TOPOLOGY } from '../actions/topologies' -import { onAddPrefab } from './prefabs' -import { ADD_PREFAB } from '../actions/prefabs' +import { fork } from 'redux-saga/effects' +import { watchServer, updateServer } from './topology' export default function* rootSaga() { - yield takeEvery(OPEN_TOPOLOGY, onOpenTopology) - - yield takeEvery(ADD_TOPOLOGY, onAddTopology) - yield takeEvery(START_NEW_ROOM_CONSTRUCTION, onStartNewRoomConstruction) - yield takeEvery(CANCEL_NEW_ROOM_CONSTRUCTION, onCancelNewRoomConstruction) - yield takeEvery(ADD_TILE, onAddTile) - yield takeEvery(DELETE_TILE, onDeleteTile) - yield takeEvery(EDIT_ROOM_NAME, onEditRoomName) - yield takeEvery(DELETE_ROOM, onDeleteRoom) - yield takeEvery(EDIT_RACK_NAME, onEditRackName) - yield takeEvery(DELETE_RACK, onDeleteRack) - yield takeEvery(ADD_RACK_TO_TILE, onAddRackToTile) - yield takeEvery(ADD_MACHINE, onAddMachine) - yield takeEvery(DELETE_MACHINE, onDeleteMachine) - yield takeEvery(ADD_UNIT, onAddUnit) - yield takeEvery(DELETE_UNIT, onDeleteUnit) - - yield takeEvery(ADD_PREFAB, onAddPrefab) + yield fork(watchServer) + yield fork(updateServer) } diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js b/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js deleted file mode 100644 index f717d878..00000000 --- a/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js +++ /dev/null @@ -1,18 +0,0 @@ -import { call, put, select, getContext } from 'redux-saga/effects' -import { addToStore } from '../actions/objects' -import { addPrefab } from '../../api/prefabs' -import { Rack } from '../../util/topology-schema' -import { denormalize } from 'normalizr' - -export function* onAddPrefab({ name, tileId }) { - try { - const objects = yield select((state) => state.objects) - const rack = objects.rack[objects.tile[tileId].rack] - const prefabRack = denormalize(rack, Rack, objects) - const auth = yield getContext('auth') - const prefab = yield call(() => addPrefab(auth, { name, rack: prefabRack })) - yield put(addToStore('prefab', prefab)) - } catch (error) { - console.error(error) - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js index fb6f7f0d..f40cff28 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -1,287 +1,75 @@ +import { QueryObserver, MutationObserver } from 'react-query' import { normalize, denormalize } from 'normalizr' -import { put, select } from 'redux-saga/effects' +import { select, put, take, race, getContext, call } from 'redux-saga/effects' +import { eventChannel } from 'redux-saga' import { Topology } from '../../util/topology-schema' -import { goDownOneInteractionLevel } from '../actions/interaction-level' -import { - addIdToStoreObjectListProp, - addPropToStoreObject, - addToStore, - removeIdFromStoreObjectListProp, -} from '../actions/objects' -import { storeTopology } from '../actions/topologies' -import { - cancelNewRoomConstructionSucceeded, - setCurrentTopology, - startNewRoomConstructionSucceeded, -} from '../actions/topology/building' -import { - DEFAULT_RACK_POWER_CAPACITY, - DEFAULT_RACK_SLOT_CAPACITY, - MAX_NUM_UNITS_PER_MACHINE, -} from '../../components/topologies/map/MapConstants' -import { uuid } from 'uuidv4' -import { fetchQuery, mutate } from './query' +import { storeTopology, OPEN_TOPOLOGY } from '../actions/topologies' /** - * Fetches and normalizes the topology with the specified identifier. + * Update the topology on the server. */ -function* fetchAndStoreTopology(id) { - let topology = yield select((state) => state.objects.topology[id]) - if (!topology) { - const newTopology = yield fetchQuery(['topologies', id]) - const { entities } = normalize(newTopology, Topology) - yield put(storeTopology(entities)) - } - - return topology -} - -/** - * Synchronize the topology with the specified identifier with the server. - */ -function* updateTopologyOnServer(id) { - const topology = yield denormalizeTopology(id) - yield mutate('updateTopology', topology) -} - -/** - * Denormalizes the topology representation in order to be stored on the server. - */ -function* denormalizeTopology(id) { - const objects = yield select((state) => state.objects) - const topology = objects.topology[id] - return denormalize(topology, Topology, objects) -} - -export function* onOpenTopology({ id }) { - try { - yield fetchAndStoreTopology(id) - yield put(setCurrentTopology(id)) - } catch (error) { - console.error(error) - } -} - -export function* onAddTopology({ projectId, duplicateId, name }) { - try { - let topologyToBeCreated - if (duplicateId) { - topologyToBeCreated = yield denormalizeTopology(duplicateId) - topologyToBeCreated = { ...topologyToBeCreated, name } - delete topologyToBeCreated._id - } else { - topologyToBeCreated = { name, rooms: [] } - } - - const topology = yield mutate('addTopology', { ...topologyToBeCreated, projectId }) - yield fetchAndStoreTopology(topology._id) - yield put(setCurrentTopology(topology._id)) - } catch (error) { - console.error(error) - } -} - -export function* onStartNewRoomConstruction() { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const room = { - _id: uuid(), - name: 'Room', - topologyId, - tiles: [], - } - yield put(addToStore('room', room)) - yield put(addIdToStoreObjectListProp('topology', topologyId, 'rooms', room._id)) - yield updateTopologyOnServer(topologyId) - yield put(startNewRoomConstructionSucceeded(room._id)) - } catch (error) { - console.error(error) - } -} - -export function* onCancelNewRoomConstruction() { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const roomId = yield select((state) => state.construction.currentRoomInConstruction) - yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'rooms', roomId)) - // TODO remove room from store, too - yield updateTopologyOnServer(topologyId) - yield put(cancelNewRoomConstructionSucceeded()) - } catch (error) { - console.error(error) - } -} +export function* updateServer() { + const queryClient = yield getContext('queryClient') + const mutationObserver = new MutationObserver(queryClient, { mutationKey: 'updateTopology' }) + + while (true) { + yield take( + (action) => + action.type.startsWith('EDIT') || action.type.startsWith('ADD') || action.type.startsWith('DELETE') + ) + const topology = yield select((state) => state.topology) -export function* onAddTile(action) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const roomId = yield select((state) => state.construction.currentRoomInConstruction) - const tile = { - _id: uuid(), - roomId, - positionX: action.positionX, - positionY: action.positionY, + if (!topology.root) { + continue } - yield put(addToStore('tile', tile)) - yield put(addIdToStoreObjectListProp('room', roomId, 'tiles', tile._id)) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onDeleteTile(action) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const roomId = yield select((state) => state.construction.currentRoomInConstruction) - yield put(removeIdFromStoreObjectListProp('room', roomId, 'tiles', action.tileId)) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} -export function* onEditRoomName({ roomId, name }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - yield put(addPropToStoreObject('room', roomId, { name })) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) + const denormalizedTopology = denormalize(topology.root, Topology, topology) + yield call([mutationObserver, mutationObserver.mutate], denormalizedTopology) } } -export function* onDeleteRoom({ roomId }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - yield put(goDownOneInteractionLevel()) - yield put(removeIdFromStoreObjectListProp('topology', topologyId, 'rooms', roomId)) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onEditRackName({ rackId, name }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - yield put(addPropToStoreObject('rack', rackId, { name })) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onDeleteRack({ tileId }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - yield put(goDownOneInteractionLevel()) - yield put(addPropToStoreObject('tile', tileId, { rack: undefined })) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onAddRackToTile({ tileId }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const rack = { - _id: uuid(), - name: 'Rack', - tileId, - capacity: DEFAULT_RACK_SLOT_CAPACITY, - powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, - machines: [], - } - yield put(addToStore('rack', rack)) - yield put(addPropToStoreObject('tile', tileId, { rack: rack._id })) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onAddMachine({ rackId, position }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const rack = yield select((state) => state.objects.rack[rackId]) - - const machine = { - _id: uuid(), - rackId, - position, - cpus: [], - gpus: [], - memories: [], - storages: [], +/** + * Watch the topology on the server for changes. + */ +export function* watchServer() { + let { id } = yield take(OPEN_TOPOLOGY) + while (true) { + const channel = yield queryObserver(id) + + while (true) { + const [action, response] = yield race([take(OPEN_TOPOLOGY), take(channel)]) + + if (action) { + id = action.id + break + } + + const { isFetched, data } = response + // Only update the topology on the client-side when a new topology was fetched + if (isFetched) { + const { result: topologyId, entities } = normalize(data, Topology) + yield put(storeTopology(entities.topologies[topologyId], entities)) + } } - yield put(addToStore('machine', machine)) - - const machineIds = [...rack.machines, machine._id] - yield put(addPropToStoreObject('rack', rackId, { machines: machineIds })) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} - -export function* onDeleteMachine({ rackId, position }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const rack = yield select((state) => state.objects.rack[rackId]) - yield put(goDownOneInteractionLevel()) - yield put( - addPropToStoreObject('rack', rackId, { machines: rack.machines.filter((_, idx) => idx !== position - 1) }) - ) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) } } -const unitMapping = { - cpu: 'cpus', - gpu: 'gpus', - memory: 'memories', - storage: 'storages', -} - -export function* onAddUnit({ machineId, unitType, id }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const machine = yield select((state) => state.objects.machine[machineId]) - - if (machine[unitMapping[unitType]].length >= MAX_NUM_UNITS_PER_MACHINE) { - return - } +/** + * Observe changes for the topology with the specified identifier. + */ +function* queryObserver(id) { + const queryClient = yield getContext('queryClient') + const observer = new QueryObserver(queryClient, { queryKey: ['topologies', id] }) - const units = [...machine[unitMapping[unitType]], id] - yield put( - addPropToStoreObject('machine', machine._id, { - [unitMapping[unitType]]: units, - }) - ) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } -} + return eventChannel((emitter) => { + const unsubscribe = observer.subscribe((result) => { + emitter(result) + }) -export function* onDeleteUnit({ machineId, unitType, index }) { - try { - const topologyId = yield select((state) => state.currentTopologyId) - const machine = yield select((state) => state.objects.machine[machineId]) - const unitIds = machine[unitMapping[unitType]].slice() - unitIds.splice(index, 1) + // Update result to make sure we did not miss any query updates + // between creating the observer and subscribing to it. + observer.updateResult() - yield put( - addPropToStoreObject('machine', machine._id, { - [unitMapping[unitType]]: unitIds, - }) - ) - yield updateTopologyOnServer(topologyId) - } catch (error) { - console.error(error) - } + return unsubscribe + }) } diff --git a/opendc-web/opendc-web-ui/src/util/topology-schema.js b/opendc-web/opendc-web-ui/src/util/topology-schema.js index 9acd688b..7779ccfe 100644 --- a/opendc-web/opendc-web-ui/src/util/topology-schema.js +++ b/opendc-web/opendc-web-ui/src/util/topology-schema.js @@ -22,13 +22,13 @@ import { schema } from 'normalizr' -const Cpu = new schema.Entity('cpu', {}, { idAttribute: '_id' }) -const Gpu = new schema.Entity('gpu', {}, { idAttribute: '_id' }) -const Memory = new schema.Entity('memory', {}, { idAttribute: '_id' }) -const Storage = new schema.Entity('storage', {}, { idAttribute: '_id' }) +const Cpu = new schema.Entity('cpus', {}, { idAttribute: '_id' }) +const Gpu = new schema.Entity('gpus', {}, { idAttribute: '_id' }) +const Memory = new schema.Entity('memories', {}, { idAttribute: '_id' }) +const Storage = new schema.Entity('storages', {}, { idAttribute: '_id' }) export const Machine = new schema.Entity( - 'machine', + 'machines', { cpus: [Cpu], gpus: [Gpu], @@ -38,10 +38,10 @@ export const Machine = new schema.Entity( { idAttribute: '_id' } ) -export const Rack = new schema.Entity('rack', { machines: [Machine] }, { idAttribute: '_id' }) +export const Rack = new schema.Entity('racks', { machines: [Machine] }, { idAttribute: '_id' }) -export const Tile = new schema.Entity('tile', { rack: Rack }, { idAttribute: '_id' }) +export const Tile = new schema.Entity('tiles', { rack: Rack }, { idAttribute: '_id' }) -export const Room = new schema.Entity('room', { tiles: [Tile] }, { idAttribute: '_id' }) +export const Room = new schema.Entity('rooms', { tiles: [Tile] }, { idAttribute: '_id' }) -export const Topology = new schema.Entity('topology', { rooms: [Room] }, { idAttribute: '_id' }) +export const Topology = new schema.Entity('topologies', { rooms: [Room] }, { idAttribute: '_id' }) diff --git a/opendc-web/opendc-web-ui/yarn.lock b/opendc-web/opendc-web-ui/yarn.lock index 78122b5d..062766dd 100644 --- a/opendc-web/opendc-web-ui/yarn.lock +++ b/opendc-web/opendc-web-ui/yarn.lock @@ -2120,6 +2120,11 @@ image-size@1.0.0: dependencies: queue "6.0.2" +immer@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.5.tgz#a7154f34fe7064f15f00554cc94c66cc0bf453ec" + integrity sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ== + import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" |
