From 048bf777997bdbf599240645fc66612c98abf3c2 Mon Sep 17 00:00:00 2001 From: vincent van beek Date: Fri, 27 Mar 2026 16:49:40 +0100 Subject: Add import topology (#393) * add a the posibility to import and export topogies in JSON format * fix web-runner integration, there were several bugs and mismatches between new implementations in OpenDC and the UI --- .../main/java/org/opendc/web/server/model/Job.java | 33 ++---- .../java/org/opendc/web/server/model/Topology.java | 19 ++- .../opendc/web/server/rest/SchedulerResource.java | 23 ++-- .../web/server/rest/user/TopologyResource.java | 126 +++++++++++++++++++- .../src/main/resources/load_data.sql | 4 +- .../webui/components/projects/ImportTopology.js | 59 ++++++++++ .../components/projects/ImportTopologyModal.js | 131 +++++++++++++++++++++ .../webui/components/projects/NewTopologyModal.js | 11 +- .../webui/components/projects/ProjectOverview.js | 2 + .../webui/components/projects/TopologyTable.js | 32 ++++- .../main/webui/redux/reducers/construction-mode.js | 7 ++ .../main/webui/redux/reducers/interaction-level.js | 3 + .../main/webui/redux/reducers/topology/index.js | 26 +++- .../web/server/rest/user/TopologyResourceTest.java | 17 +++ 14 files changed, 448 insertions(+), 45 deletions(-) create mode 100644 opendc-web/opendc-web-server/src/main/webui/components/projects/ImportTopology.js create mode 100644 opendc-web/opendc-web-server/src/main/webui/components/projects/ImportTopologyModal.js (limited to 'opendc-web/opendc-web-server/src') diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java index ef342e5f..63982854 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java @@ -26,7 +26,6 @@ import io.hypersistence.utils.hibernate.type.json.JsonType; import io.quarkus.hibernate.orm.panache.Panache; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import io.quarkus.hibernate.orm.panache.PanacheQuery; -import io.quarkus.panache.common.Parameters; import jakarta.persistence.*; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -36,8 +35,6 @@ import jakarta.persistence.FetchType; import jakarta.persistence.ForeignKey; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.NamedQueries; -import jakarta.persistence.NamedQuery; import jakarta.persistence.Table; import java.time.Instant; import java.util.Map; @@ -49,16 +46,6 @@ import org.opendc.web.proto.JobState; */ @Entity @Table -@NamedQueries({ - @NamedQuery( - name = "Job.updateOne", - query = - """ - UPDATE Job j - SET j.state = :newState, j.updatedAt = :updatedAt, j.runtime = :runtime, j.results = :results - WHERE j.id = :id AND j.state = :oldState - """) -}) public class Job extends PanacheEntityBase { /** * The main ID of a project. @@ -146,16 +133,16 @@ public class Job extends PanacheEntityBase { * @return true when the update succeeded`, false when there was a conflict. */ public boolean updateAtomically(JobState newState, Instant time, int runtime, Map results) { - long count = update( - "#Job.updateOne", - Parameters.with("id", id) - .and("oldState", state) - .and("newState", newState) - .and("updatedAt", time) - .and("runtime", runtime) - .and("results", results)); - Panache.getEntityManager().refresh(this); - return count > 0; + // Update entity fields directly - this uses the JsonType converter for proper JSON serialization + this.state = newState; + this.updatedAt = time; + this.runtime = runtime; + this.results = results; + + // Flush changes to database - JsonType will properly serialize the results map to JSON + Panache.getEntityManager().flush(); + + return true; } /** diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java index ff8b4416..50402ab9 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java @@ -59,7 +59,10 @@ import org.opendc.web.proto.topology.Room; @NamedQuery(name = "Topology.findByProject", query = "SELECT t FROM Topology t WHERE t.project.id = :projectId"), @NamedQuery( name = "Topology.findOneByProject", - query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.number = :number") + query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.number = :number"), + @NamedQuery( + name = "Topology.findOneByName", + query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.name = :name") }) public class Topology extends PanacheEntityBase { /** @@ -149,4 +152,18 @@ public class Topology extends PanacheEntityBase { Parameters.with("projectId", projectId).and("number", number)) .firstResult(); } + + /** + * Find the [Topology] with the specified [name] belonging to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @param name The name of the topology. + * @return The topology or `null` if it does not exist. + */ + public static Topology findByName(long projectId, String name) { + return find( + "#Topology.findOneByName", + Parameters.with("projectId", projectId).and("name", name)) + .firstResult(); + } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java index 3e839040..527c04b1 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java @@ -39,14 +39,19 @@ public final class SchedulerResource { @GET public List getAll() { return List.of( - "mem", - "mem-inv", - "core-mem", - "core-mem-inv", - "active-tasks", - "active-tasks-inv", - "provisioned-cores", - "provisioned-cores-inv", - "random"); + "Mem", + "MemInv", + "CoreMem", + "CoreMemInv", + "ActiveServers", + "ActiveServersInv", + "ProvisionedCores", + "ProvisionedCoresInv", + "Random", + "TaskNumMemorizing", + "Timeshift", + "ProvisionedCpuGpuCores", + "ProvisionedCpuGpuCoresInv", + "GpuTaskMemorizing"); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java index 25819e32..79290e26 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java @@ -39,6 +39,15 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.WebApplicationException; import java.time.Instant; import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.opendc.web.proto.topology.Machine; +import org.opendc.web.proto.topology.MemoryUnit; +import org.opendc.web.proto.topology.ProcessingUnit; +import org.opendc.web.proto.topology.Rack; +import org.opendc.web.proto.topology.Room; +import org.opendc.web.proto.topology.RoomTile; import org.opendc.web.server.model.Project; import org.opendc.web.server.model.ProjectAuthorization; import org.opendc.web.server.model.Topology; @@ -102,9 +111,14 @@ public final class TopologyResource { Instant now = Instant.now(); Project project = auth.project; + + if (Topology.findByName(projectId, request.name()) != null) { + throw new WebApplicationException("Topology name already exists", 409); + } + int number = project.allocateTopology(now); - Topology topology = new Topology(project, number, request.name(), now, request.rooms()); + Topology topology = new Topology(project, number, request.name(), now, createNewInstances(request.rooms())); project.topologies.add(topology); topology.persist(); @@ -205,4 +219,114 @@ public final class TopologyResource { return UserProtocol.toDto(entity, auth); } + + /** + * Create new instances of the specified rooms. + */ + private List createNewInstances(List rooms) { + if (rooms == null) { + return null; + } + + return rooms.stream().map(this::createNewInstance).toList(); + } + + /** + * Create a new instance of the specified room. + */ + private Room createNewInstance(Room room) { + String roomId = UUID.randomUUID().toString(); + Set tiles = room.tiles(); + if (tiles != null) { + tiles = tiles.stream().map(tile -> createNewInstance(tile, roomId)).collect(Collectors.toSet()); + } + + return new Room(roomId, room.name(), tiles, room.topologyId()); + } + + /** + * Create a new instance of the specified room tile. + */ + private RoomTile createNewInstance(RoomTile tile, String roomId) { + String tileId = UUID.randomUUID().toString(); + return new RoomTile( + tileId, + tile.positionX(), + tile.positionY(), + tile.rack() != null ? createNewInstance(tile.rack()) : null, + roomId); + } + + /** + * Create a new instance of the specified rack. + */ + private Rack createNewInstance(Rack rack) { + String rackId = UUID.randomUUID().toString(); + List machines = rack.machines(); + if (machines != null) { + machines = machines.stream() + .map(machine -> createNewInstance(machine, rackId)) + .toList(); + } + + return new Rack(rackId, rack.name(), rack.capacity(), rack.powerCapacityW(), machines); + } + + /** + * Create a new instance of the specified machine. + */ + private Machine createNewInstance(Machine machine, String rackId) { + String machineId = UUID.randomUUID().toString(); + List cpus = machine.cpus(); + if (cpus != null) { + cpus = cpus.stream().map(this::createNewInstance).toList(); + } + + List gpus = machine.gpus(); + if (gpus != null) { + gpus = gpus.stream().map(this::createNewInstance).toList(); + } + + List memory = machine.memory(); + if (memory != null) { + memory = memory.stream().map(this::createNewInstance).toList(); + } + + List storage = machine.storage(); + if (storage != null) { + storage = storage.stream().map(this::createNewInstance).toList(); + } + + return new Machine(machineId, machine.position(), cpus, gpus, memory, storage, rackId); + } + + /** + * Create a new instance of the specified processing unit. + */ + private ProcessingUnit createNewInstance(ProcessingUnit unit) { + if (unit != null && (unit.id() == null || unit.id().isBlank())) { + return new ProcessingUnit( + UUID.randomUUID().toString(), + unit.name(), + unit.clockRateMhz(), + unit.numberOfCores(), + unit.energyConsumptionW()); + } + return unit; + } + + /** + * Create a new instance of the specified memory unit. + */ + private MemoryUnit createNewInstance(MemoryUnit unit) { + if (unit != null && (unit.id() == null || unit.id().isBlank())) { + return new MemoryUnit( + UUID.randomUUID().toString(), + unit.name(), + unit.speedMbPerS(), + unit.sizeMb(), + unit.energyConsumptionW()); + } + return unit; + } } diff --git a/opendc-web/opendc-web-server/src/main/resources/load_data.sql b/opendc-web/opendc-web-server/src/main/resources/load_data.sql index ca505b80..df3e3709 100644 --- a/opendc-web/opendc-web-server/src/main/resources/load_data.sql +++ b/opendc-web/opendc-web-server/src/main/resources/load_data.sql @@ -103,10 +103,10 @@ VALUES ('bitbrains-small', 'Bitbrains Small', 'small'); -- -------------------------------------------------------------------------------- INSERT INTO scenarios (name, number, phenomena, portfolio_id, project_id, scheduler_name, topology_id, sampling_fraction, trace_id, id) -VALUES ('Test Scenario testDelete', 1, '{"failures": false, "interference": false}' FORMAT JSON, 1, 1, 'test', 1, 1.0, 'bitbrains-small', 1); +VALUES ('Test Scenario testDelete', 1, '{"failures": false, "interference": false}' FORMAT JSON, 1, 1, 'Mem', 1, 1.0, 'bitbrains-small', 1); INSERT INTO scenarios (name, number, phenomena, portfolio_id, project_id, scheduler_name, topology_id, sampling_fraction, trace_id, id) -VALUES ('Test Scenario testDeleteUsed', 2, '{"failures": false, "interference": false}' FORMAT JSON, 1, 1, 'test', 4, 1.0, 'bitbrains-small', 2); +VALUES ('Test Scenario testDeleteUsed', 2, '{"failures": false, "interference": false}' FORMAT JSON, 1, 1, 'Random', 4, 1.0, 'bitbrains-small', 2); UPDATE projects p diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/ImportTopology.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/ImportTopology.js new file mode 100644 index 00000000..715c1a3e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/ImportTopology.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 React, { useState } from 'react' +import PropTypes from 'prop-types' +import { UploadIcon } from '@patternfly/react-icons' +import { Button } from '@patternfly/react-core' +import { useNewTopology, useTopologies } from '../../data/topology' +import ImportTopologyModal from './ImportTopologyModal' + +function ImportTopology({ projectId }) { + const [isVisible, setVisible] = useState(false) + const { mutate: addTopology } = useNewTopology() + const { data: topologies = [] } = useTopologies(projectId, { enabled: isVisible }) + + const onSubmit = (topology) => { + addTopology({ projectId, ...topology }) + setVisible(false) + } + + return ( + <> + + setVisible(false)} + onSubmit={onSubmit} + topologies={topologies} + /> + + ) +} + +ImportTopology.propTypes = { + projectId: PropTypes.number, +} + +export default ImportTopology diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/ImportTopologyModal.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/ImportTopologyModal.js new file mode 100644 index 00000000..d8f38426 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/ImportTopologyModal.js @@ -0,0 +1,131 @@ +/* + * 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 React, { useState } from 'react' +import PropTypes from 'prop-types' +import { + Form, + FormGroup, + FileUpload, + Alert, +} from '@patternfly/react-core' +import Modal from '../util/modals/Modal' + +function ImportTopologyModal({ isOpen, onCancel, onSubmit, topologies = [] }) { + const [fileValue, setFileValue] = useState('') + const [fileName, setFileName] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const handleFileChange = (value, filename) => { + setFileValue(value) + setFileName(filename) + } + + const handleFileReadStarted = () => setIsLoading(true) + const handleFileReadFinished = () => setIsLoading(false) + + const clearState = () => { + setFileValue('') + setFileName('') + setError(null) + } + + const onCancelProxy = () => { + onCancel() + clearState() + } + + const handleFormSubmit = (event) => { + if (event) { + event.preventDefault() + } + + if (!fileValue) { + setError('Please upload a topology JSON file.') + return + } + + try { + const topology = JSON.parse(fileValue) + if (!topology.name || !topology.rooms) { + setError('Invalid topology format. Name and rooms are required.') + return + } + + if (topologies.some((t) => t.name === topology.name)) { + setError('A topology with the name "' + topology.name + '" already exists.') + return + } + + onSubmit(topology) + clearState() + } catch (err) { + setError('Failed to parse JSON: ' + err.message) + } + } + + return ( + +
+ {error && ( + + )} + + + + +
+ ) +} + +ImportTopologyModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onCancel: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + topologies: PropTypes.array, +} + +export default ImportTopologyModal diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopologyModal.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopologyModal.js index 780ec034..68b7f150 100644 --- a/opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopologyModal.js +++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/NewTopologyModal.js @@ -54,7 +54,10 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan const name = nameInput.current.value if (!name) { - setErrors({ name: true }) + setErrors({ name: 'empty' }) + return false + } else if (topologies.some((topology) => topology.name === name)) { + setErrors({ name: 'duplicate' }) return false } else { const candidate = topologies.find((topology) => topology.id === originTopology) || { rooms: [] } @@ -83,7 +86,11 @@ const NewTopologyModal = ({ projectId, isOpen, onSubmit: onSubmitUpstream, onCan fieldId="name" isRequired validated={isSubmitted && errors.name ? 'error' : 'default'} - helperTextInvalid="This field cannot be empty" + helperTextInvalid={ + errors.name === 'empty' + ? 'This field cannot be empty' + : 'This topology name already exists' + } > diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectOverview.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectOverview.js index 3e1656f6..6800f401 100644 --- a/opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectOverview.js +++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/ProjectOverview.js @@ -36,6 +36,7 @@ import { Skeleton, } from '@patternfly/react-core' import NewTopology from './NewTopology' +import ImportTopology from './ImportTopology' import TopologyTable from './TopologyTable' import NewPortfolio from './NewPortfolio' import PortfolioTable from './PortfolioTable' @@ -65,6 +66,7 @@ function ProjectOverview({ projectId }) { + Topologies diff --git a/opendc-web/opendc-web-server/src/main/webui/components/projects/TopologyTable.js b/opendc-web/opendc-web-server/src/main/webui/components/projects/TopologyTable.js index 1c2c4f04..89a28889 100644 --- a/opendc-web/opendc-web-server/src/main/webui/components/projects/TopologyTable.js +++ b/opendc-web/opendc-web-server/src/main/webui/components/projects/TopologyTable.js @@ -37,11 +37,37 @@ function TopologyTable({ projectId }) { onError: (error) => setError(error), }) - const actions = ({ number }) => [ + const downloadTopology = (topology) => { + const data = JSON.stringify( + { name: topology.name, rooms: topology.rooms }, + (key, value) => { + if (key === 'id' || key === 'topologyId' || key === 'roomId' || key === 'rackId' || key === 'roomid') { + return undefined + } + return value + }, + 4 + ) + const blob = new Blob([data], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `${topology.name}.json` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } + + const actions = (topology) => [ + { + title: 'Download as JSON', + onClick: () => downloadTopology(topology), + }, { title: 'Delete Topology', - onClick: () => deleteTopology({ projectId, number }), - isDisabled: number === 0, + onClick: () => deleteTopology({ projectId, number: topology.number }), + isDisabled: topology.number === 0, }, ] diff --git a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js index 8520e794..61265b1e 100644 --- a/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js +++ b/opendc-web/opendc-web-server/src/main/webui/redux/reducers/construction-mode.js @@ -1,5 +1,6 @@ import { combineReducers } from 'redux' import { GO_DOWN_ONE_INTERACTION_LEVEL } from '../actions/interaction-level' +import { OPEN_TOPOLOGY } from '../actions/topology' import { CANCEL_NEW_ROOM_CONSTRUCTION_SUCCEEDED, FINISH_NEW_ROOM_CONSTRUCTION, @@ -11,6 +12,8 @@ import { DELETE_ROOM, START_RACK_CONSTRUCTION, STOP_RACK_CONSTRUCTION } from '.. export function currentRoomInConstruction(state = '-1', action) { switch (action.type) { + case OPEN_TOPOLOGY: + return '-1' case START_NEW_ROOM_CONSTRUCTION_SUCCEEDED: return action.roomId case START_ROOM_EDIT: @@ -27,6 +30,8 @@ export function currentRoomInConstruction(state = '-1', action) { export function inRackConstructionMode(state = false, action) { switch (action.type) { + case OPEN_TOPOLOGY: + return false case START_RACK_CONSTRUCTION: return true case STOP_RACK_CONSTRUCTION: @@ -39,6 +44,8 @@ export function inRackConstructionMode(state = false, action) { export function currentRackPrefab(state = null, action) { switch (action.type) { + case OPEN_TOPOLOGY: + return null case START_RACK_CONSTRUCTION: return action.rackPrefab || null case STOP_RACK_CONSTRUCTION: 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 index b30c68b9..d1342d3d 100644 --- 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 @@ -4,12 +4,15 @@ import { GO_FROM_RACK_TO_MACHINE, GO_FROM_ROOM_TO_RACK, } from '../actions/interaction-level' +import { OPEN_TOPOLOGY } from '../actions/topology' 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 OPEN_TOPOLOGY: + return { mode: 'BUILDING' } case GO_FROM_BUILDING_TO_ROOM: return { mode: 'ROOM', 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 index 2c849387..18d6cde4 100644 --- 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 @@ -21,18 +21,36 @@ */ import { CPU_UNITS, GPU_UNITS, MEMORY_UNITS, STORAGE_UNITS } from '../../../util/unit-specifications' +import { STORE_TOPOLOGY } from '../../actions/topology' +import { ADD_RACK_TO_TILE } from '../../actions/topology/room' import machine from './machine' import rack from './rack' import room from './room' import tile from './tile' import topology from './topology' +function unitReducer(defaultUnits, entityType) { + return (state = defaultUnits, action) => { + if (action.type === STORE_TOPOLOGY) { + return { ...defaultUnits, ...((action.entities && action.entities[entityType]) || {}) } + } else if (action.type === ADD_RACK_TO_TILE) { + return { ...state, ...((action.entities && action.entities[entityType]) || {}) } + } + return state + } +} + +const cpus = unitReducer(CPU_UNITS, 'cpus') +const gpus = unitReducer(GPU_UNITS, 'gpus') +const memories = unitReducer(MEMORY_UNITS, 'memories') +const storages = unitReducer(STORAGE_UNITS, 'storages') + function objects(state = {}, action) { return { - cpus: CPU_UNITS, - gpus: GPU_UNITS, - memories: MEMORY_UNITS, - storages: STORAGE_UNITS, + cpus: cpus(state.cpus, action), + gpus: gpus(state.gpus, action), + memories: memories(state.memories, action), + storages: storages(state.storages, action), machines: machine(state.machines, action, state), racks: rack(state.racks, action, state), tiles: tile(state.tiles, action), diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java index 277376e5..b3b668dd 100644 --- a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java @@ -156,6 +156,23 @@ public final class TopologyResourceTest { .statusCode(400); } + /** + * Test to create a topology with a duplicate name. + */ + @Test + @TestSecurity( + user = "test_user_1", + roles = {"openid"}) + public void testCreateDuplicateName() { + given().pathParam("project", "1") + .body(new Topology.Create("Test Topology testUpdate", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(409); + } + /** * Test that tries to obtain a topology without token. */ -- cgit v1.2.3