From 0ffde21b0337c606e2d0ece5bd5434a930a87dcd Mon Sep 17 00:00:00 2001 From: vincent van beek Date: Thu, 26 Mar 2026 14:02:54 +0100 Subject: Use Quarkus Quinoa for serving web UI (#391) * refactor(web): Migrate to Quarkus 3 This commit updates the OpenDC web server to use Quarkus 3, which changes annotations to use the Jakarta namespace for annotations. * refactor(web): Configure runtime variables for web UI This commit updates the web UI to propagate runtime variables via the next-runtime-env package. Before, we would need to replace the variables in the generated sources by Next.js, next-runtime-env works by loading a JavaScript file when opening the OpenDC web UI which contains the configuration of the web app. * refactor(web): Migrate to Quarkus Quinoa This commit updates the OpenDC web server to make use of Quarkus Quinoa for serving the web UI. This allows us to deprecate the complex Quarkus extension for serving the web UI. * refactor(web): Move web UI into Quarkus web app This commit moves the web UI into the Quarkus web server module to ensure we follow Quarkus Quinoa's conventions. * refactor(web): Merge Quarkus extension into single module This commit merges the existing Quarkus extensions into a single module to prevent build complexity. * refactor(web): Migrate web proto to Java This commit migrates the web protocol to Java and removes the dependency on Jandex Gradle. * refactor(web): Migrate to Quarkus 3 This commit updates the OpenDC web server to use Quarkus 3, which changes annotations to use the Jakarta namespace for annotations. * enable DB schema migration on DEV server * webui is not needed anymore * remove MAINTAINERS is depricated * fix quarkus.quinoa properties * revert properties change, install npm in docker image to allow building the frontend * pin postgres version, this is a best practice. Fix some properties the old ones are depricated. Added properties for local testing * fix build error * :opendc-web:opendc-web-proto:spotlessApply * fix database schema --------- Co-authored-by: Fabian Mastenbroek --- .../main/webui/redux/actions/interaction-level.js | 57 +++++++++++ .../main/webui/redux/actions/topology/building.js | 113 +++++++++++++++++++++ .../src/main/webui/redux/actions/topology/index.js | 40 ++++++++ .../main/webui/redux/actions/topology/machine.js | 28 +++++ .../src/main/webui/redux/actions/topology/rack.js | 36 +++++++ .../src/main/webui/redux/actions/topology/room.js | 74 ++++++++++++++ .../src/main/webui/redux/index.js | 59 +++++++++++ .../main/webui/redux/reducers/construction-mode.js | 43 ++++++++ .../src/main/webui/redux/reducers/index.js | 12 +++ .../main/webui/redux/reducers/interaction-level.js | 68 +++++++++++++ .../main/webui/redux/reducers/topology/index.js | 44 ++++++++ .../main/webui/redux/reducers/topology/machine.js | 47 +++++++++ .../src/main/webui/redux/reducers/topology/rack.js | 66 ++++++++++++ .../src/main/webui/redux/reducers/topology/room.js | 65 ++++++++++++ .../src/main/webui/redux/reducers/topology/tile.js | 58 +++++++++++ .../main/webui/redux/reducers/topology/topology.js | 47 +++++++++ .../src/main/webui/redux/sagas/index.js | 7 ++ .../src/main/webui/redux/sagas/topology.js | 76 ++++++++++++++ 18 files changed, 940 insertions(+) create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/actions/interaction-level.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/building.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/index.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/machine.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/rack.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/room.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/index.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/reducers/index.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/reducers/interaction-level.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/index.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/machine.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/rack.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/room.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/tile.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/topology.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/sagas/index.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/redux/sagas/topology.js (limited to 'opendc-web/opendc-web-server/src/main/webui/redux') diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/interaction-level.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/interaction-level.js new file mode 100644 index 00000000..8381eeef --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/interaction-level.js @@ -0,0 +1,57 @@ +export const GO_FROM_BUILDING_TO_ROOM = 'GO_FROM_BUILDING_TO_ROOM' +export const GO_FROM_ROOM_TO_RACK = 'GO_FROM_ROOM_TO_RACK' +export const GO_FROM_RACK_TO_MACHINE = 'GO_FROM_RACK_TO_MACHINE' +export const GO_DOWN_ONE_INTERACTION_LEVEL = 'GO_DOWN_ONE_INTERACTION_LEVEL' + +export function goToRoom(roomId) { + return { + type: GO_FROM_BUILDING_TO_ROOM, + roomId, + } +} + +export function goFromBuildingToRoom(roomId) { + return (dispatch, getState) => { + const { interactionLevel } = getState() + if (interactionLevel.mode !== 'BUILDING') { + return + } + + dispatch({ + type: GO_FROM_BUILDING_TO_ROOM, + roomId, + }) + } +} + +export function goFromRoomToRack(tileId) { + return (dispatch, getState) => { + const { interactionLevel } = getState() + if (interactionLevel.mode !== 'ROOM') { + return + } + dispatch({ + type: GO_FROM_ROOM_TO_RACK, + tileId, + }) + } +} + +export function goFromRackToMachine(position) { + return (dispatch, getState) => { + const { interactionLevel } = getState() + if (interactionLevel.mode !== 'RACK') { + return + } + dispatch({ + type: GO_FROM_RACK_TO_MACHINE, + position, + }) + } +} + +export function goDownOneInteractionLevel() { + return { + type: GO_DOWN_ONE_INTERACTION_LEVEL, + } +} diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/building.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/building.js new file mode 100644 index 00000000..c12417b9 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/building.js @@ -0,0 +1,113 @@ +import { v4 as uuid } from 'uuid' +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' +export const CANCEL_NEW_ROOM_CONSTRUCTION = 'CANCEL_NEW_ROOM_CONSTRUCTION' +export const CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED = 'CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED' +export const START_ROOM_EDIT = 'START_ROOM_EDIT' +export const FINISH_ROOM_EDIT = 'FINISH_ROOM_EDIT' +export const ADD_TILE = 'ADD_TILE' +export const DELETE_TILE = 'DELETE_TILE' + +export function startNewRoomConstruction() { + 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)) + } +} + +export function startNewRoomConstructionSucceeded(roomId) { + return { + type: START_NEW_ROOM_CONSTRUCTION_SUCCEEDED, + roomId, + } +} + +export function finishNewRoomConstruction() { + return (dispatch, getState) => { + const { topology, construction } = getState() + if (topology.rooms[construction.currentRoomInConstruction].tiles.length === 0) { + dispatch(cancelNewRoomConstruction()) + return + } + + dispatch({ + type: FINISH_NEW_ROOM_CONSTRUCTION, + }) + } +} + +export function cancelNewRoomConstruction() { + return (dispatch, getState) => { + const { construction } = getState() + const roomId = construction.currentRoomInConstruction + dispatch(deleteRoom(roomId)) + dispatch(cancelNewRoomConstructionSucceeded()) + } +} + +export function cancelNewRoomConstructionSucceeded() { + return { + type: CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED, + } +} + +export function startRoomEdit(roomId) { + return { + type: START_ROOM_EDIT, + roomId: roomId, + } +} + +export function finishRoomEdit() { + return { + type: FINISH_ROOM_EDIT, + } +} + +export function toggleTileAtLocation(positionX, positionY) { + return (dispatch, getState) => { + const { topology, construction } = getState() + + const roomId = construction.currentRoomInConstruction + const tileIds = topology.rooms[roomId].tiles + for (const tileId of tileIds) { + if (topology.tiles[tileId].positionX === positionX && topology.tiles[tileId].positionY === positionY) { + dispatch(deleteTile(tileId)) + return + } + } + + dispatch(addTile(roomId, positionX, positionY)) + } +} + +export function addTile(roomId, positionX, positionY) { + return { + type: ADD_TILE, + tile: { + id: uuid(), + roomId, + positionX, + positionY, + }, + } +} + +export function deleteTile(tileId) { + return { + type: DELETE_TILE, + tileId, + } +} diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/index.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/index.js new file mode 100644 index 00000000..d48af37a --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/index.js @@ -0,0 +1,40 @@ +/* + * 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. + */ + +export const OPEN_TOPOLOGY = 'OPEN_TOPOLOGY' +export const STORE_TOPOLOGY = 'STORE_TOPOLOGY' + +export function openTopology(projectId, id) { + return { + type: OPEN_TOPOLOGY, + projectId, + id, + } +} + +export function storeTopology(topology, entities) { + return { + type: STORE_TOPOLOGY, + topology, + entities, + } +} diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/machine.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/machine.js new file mode 100644 index 00000000..93320884 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/machine.js @@ -0,0 +1,28 @@ +export const DELETE_MACHINE = 'DELETE_MACHINE' +export const ADD_UNIT = 'ADD_UNIT' +export const DELETE_UNIT = 'DELETE_UNIT' + +export function deleteMachine(machineId) { + return { + type: DELETE_MACHINE, + machineId, + } +} + +export function addUnit(machineId, unitType, unitId) { + return { + type: ADD_UNIT, + machineId, + unitType, + unitId, + } +} + +export function deleteUnit(machineId, unitType, unitId) { + return { + type: DELETE_UNIT, + machineId, + unitType, + unitId, + } +} diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/rack.js b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/rack.js new file mode 100644 index 00000000..1f65952a --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/rack.js @@ -0,0 +1,36 @@ +import { v4 as uuid } from 'uuid' + +export const EDIT_RACK_NAME = 'EDIT_RACK_NAME' +export const DELETE_RACK = 'DELETE_RACK' +export const ADD_MACHINE = 'ADD_MACHINE' + +export function editRackName(rackId, name) { + return { + type: EDIT_RACK_NAME, + name, + rackId, + } +} + +export function deleteRack(tileId, rackId) { + return { + type: DELETE_RACK, + rackId, + tileId, + } +} + +export function addMachine(rackId, position) { + return { + type: ADD_MACHINE, + machine: { + id: uuid(), + rackId, + position, + cpus: [], + gpus: [], + memories: [], + storages: [], + }, + } +} 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 new file mode 100644 index 00000000..14cc126c --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/actions/topology/room.js @@ -0,0 +1,74 @@ +import { v4 as uuid } from 'uuid' +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, + name, + roomId, + } +} + +export function startRackConstruction() { + return { + type: START_RACK_CONSTRUCTION, + } +} + +export function stopRackConstruction() { + return { + type: STOP_RACK_CONSTRUCTION, + } +} + +export function addRackToTile(positionX, positionY) { + return (dispatch, getState) => { + const { 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', + capacity: DEFAULT_RACK_SLOT_CAPACITY, + powerCapacityW: DEFAULT_RACK_POWER_CAPACITY, + machines: [], + }, + }) + } + } +} + +export function deleteRoom(roomId) { + return { + type: DELETE_ROOM, + roomId, + } +} diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/index.js b/opendc-web/opendc-web-server/src/main/webui/redux/index.js new file mode 100644 index 00000000..53cd9144 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/index.js @@ -0,0 +1,59 @@ +import { useMemo } from 'react' +import { applyMiddleware, compose, createStore } from 'redux' +import { createLogger } from 'redux-logger' +import createSagaMiddleware from 'redux-saga' +import thunk from 'redux-thunk' +import rootReducer from './reducers' +import rootSaga from './sagas' +import { createReduxEnhancer } from '@sentry/react' +import { sentryDsn } from '../config' + +let store + +function initStore(initialState, ctx) { + const sagaMiddleware = createSagaMiddleware({ context: ctx }) + + const middlewares = [thunk, sagaMiddleware] + + if (process.env.NODE_ENV !== 'production') { + middlewares.push(createLogger()) + } + + let middleware = applyMiddleware(...middlewares) + + if (sentryDsn) { + middleware = compose(middleware, createReduxEnhancer()) + } + + const configuredStore = createStore(rootReducer, initialState, middleware) + sagaMiddleware.run(rootSaga) + store = configuredStore + + return configuredStore +} + +export const initializeStore = (preloadedState, ctx) => { + let _store = store ?? initStore(preloadedState, ctx) + + // After navigating to a page with an initial Redux state, merge that state + // with the current state in the store, and create a new store + if (preloadedState && store) { + _store = initStore({ + ...store.getState(), + ...preloadedState, + }) + // Reset the current store + store = undefined + } + + // For SSG and SSR always create a new store + if (typeof window === 'undefined') return _store + // Create the store once in the client + if (!store) store = _store + + return _store +} + +export function useStore(initialState, ctx) { + return useMemo(() => initializeStore(initialState, ctx), [initialState, ctx]) +} 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 new file mode 100644 index 00000000..d0aac5ae --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js @@ -0,0 +1,43 @@ +import { combineReducers } from 'redux' +import { GO_DOWN_ONE_INTERACTION_LEVEL } from '../actions/interaction-level' +import { + CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED, + FINISH_NEW_ROOM_CONSTRUCTION, + FINISH_ROOM_EDIT, + START_NEW_ROOM_CONSTRUCTION_SUCCEEDED, + START_ROOM_EDIT, +} from '../actions/topology/building' +import { DELETE_ROOM, START_RACK_CONSTRUCTION, STOP_RACK_CONSTRUCTION } from '../actions/topology/room' + +export function currentRoomInConstruction(state = '-1', action) { + switch (action.type) { + case START_NEW_ROOM_CONSTRUCTION_SUCCEEDED: + return action.roomId + case START_ROOM_EDIT: + return action.roomId + case CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED: + case FINISH_NEW_ROOM_CONSTRUCTION: + case FINISH_ROOM_EDIT: + case DELETE_ROOM: + return '-1' + default: + return state + } +} + +export function inRackConstructionMode(state = false, action) { + switch (action.type) { + case START_RACK_CONSTRUCTION: + return true + case STOP_RACK_CONSTRUCTION: + case GO_DOWN_ONE_INTERACTION_LEVEL: + return false + default: + return state + } +} + +export const construction = combineReducers({ + currentRoomInConstruction, + inRackConstructionMode, +}) diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/index.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/index.js new file mode 100644 index 00000000..7ffb1211 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/index.js @@ -0,0 +1,12 @@ +import { combineReducers } from 'redux' +import { construction } from './construction-mode' +import { interactionLevel } from './interaction-level' +import topology from './topology' + +const rootReducer = combineReducers({ + topology, + construction, + interactionLevel, +}) + +export default rootReducer diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/interaction-level.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/interaction-level.js new file mode 100644 index 00000000..b30c68b9 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/interaction-level.js @@ -0,0 +1,68 @@ +import { + GO_DOWN_ONE_INTERACTION_LEVEL, + GO_FROM_BUILDING_TO_ROOM, + GO_FROM_RACK_TO_MACHINE, + GO_FROM_ROOM_TO_RACK, +} from '../actions/interaction-level' +import { 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 GO_FROM_BUILDING_TO_ROOM: + return { + mode: 'ROOM', + roomId: action.roomId, + } + case GO_FROM_ROOM_TO_RACK: + return { + mode: 'RACK', + roomId: state.roomId, + tileId: action.tileId, + } + case GO_FROM_RACK_TO_MACHINE: + return { + mode: 'MACHINE', + roomId: state.roomId, + tileId: state.tileId, + position: action.position, + } + case GO_DOWN_ONE_INTERACTION_LEVEL: + if (state.mode === 'ROOM') { + return { + mode: 'BUILDING', + } + } else if (state.mode === 'RACK') { + return { + mode: 'ROOM', + roomId: state.roomId, + } + } else if (state.mode === 'MACHINE') { + return { + mode: 'RACK', + roomId: state.roomId, + tileId: state.tileId, + } + } else { + return state + } + 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-server/src/main/webui/redux/reducers/topology/index.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/index.js new file mode 100644 index 00000000..2c849387 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/index.js @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { CPU_UNITS, GPU_UNITS, MEMORY_UNITS, STORAGE_UNITS } from '../../../util/unit-specifications' +import machine from './machine' +import rack from './rack' +import room from './room' +import tile from './tile' +import topology from './topology' + +function objects(state = {}, action) { + return { + cpus: CPU_UNITS, + gpus: GPU_UNITS, + memories: MEMORY_UNITS, + storages: STORAGE_UNITS, + machines: machine(state.machines, action, state), + racks: rack(state.racks, action, state), + tiles: tile(state.tiles, action), + rooms: room(state.rooms, action, state), + root: topology(state.root, action, state), + } +} + +export default objects 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 new file mode 100644 index 00000000..1789257b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/machine.js @@ -0,0 +1,47 @@ +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' + +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-server/src/main/webui/redux/reducers/topology/rack.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/rack.js new file mode 100644 index 00000000..ca79348a --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/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/topology' +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-server/src/main/webui/redux/reducers/topology/room.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/room.js new file mode 100644 index 00000000..c05c8bfa --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/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/topology' +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-server/src/main/webui/redux/reducers/topology/tile.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/tile.js new file mode 100644 index 00000000..24c0e20c --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/tile.js @@ -0,0 +1,58 @@ +/* + * 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/topology' +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) { + 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, tileId } = action + draft[tileId].rack = rack.id + }) + case DELETE_RACK: + return produce(state, (draft) => { + const { tileId } = action + draft[tileId].rack = undefined + }) + default: + return state + } +} + +export default tile diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/topology.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/topology/topology.js new file mode 100644 index 00000000..dff0a69e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/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/topology' +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-server/src/main/webui/redux/sagas/index.js b/opendc-web/opendc-web-server/src/main/webui/redux/sagas/index.js new file mode 100644 index 00000000..0fabdb6d --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/sagas/index.js @@ -0,0 +1,7 @@ +import { fork } from 'redux-saga/effects' +import { watchServer, updateServer } from './topology' + +export default function* rootSaga() { + yield fork(watchServer) + yield fork(updateServer) +} diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/sagas/topology.js b/opendc-web/opendc-web-server/src/main/webui/redux/sagas/topology.js new file mode 100644 index 00000000..15147bcf --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/redux/sagas/topology.js @@ -0,0 +1,76 @@ +import { QueryObserver, MutationObserver } from 'react-query' +import { normalize, denormalize } from 'normalizr' +import { select, put, take, race, getContext, call } from 'redux-saga/effects' +import { eventChannel } from 'redux-saga' +import { Topology } from '../../util/topology-schema' +import { storeTopology, OPEN_TOPOLOGY } from '../actions/topology' + +/** + * Update the topology on the server. + */ +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) + + if (!topology.root) { + continue + } + + const denormalizedTopology = denormalize(topology.root, Topology, topology) + yield call([mutationObserver, mutationObserver.mutate], denormalizedTopology) + } +} + +/** + * Watch the topology on the server for changes. + */ +export function* watchServer() { + let { projectId, id } = yield take(OPEN_TOPOLOGY) + while (true) { + const channel = yield queryObserver(projectId, id) + + while (true) { + const [action, response] = yield race([take(OPEN_TOPOLOGY), take(channel)]) + + if (action) { + projectId = action.projectId + 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)) + } + } + } +} + +/** + * Observe changes for the topology with the specified identifier. + */ +function* queryObserver(projectId, id) { + const queryClient = yield getContext('queryClient') + const observer = new QueryObserver(queryClient, { queryKey: ['topologies', projectId, id] }) + + return eventChannel((emitter) => { + const unsubscribe = observer.subscribe((result) => { + emitter(result) + }) + + // Update result to make sure we did not miss any query updates + // between creating the observer and subscribing to it. + observer.updateResult() + + return unsubscribe + }) +} -- cgit v1.2.3