summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-server/src/main/webui/util
diff options
context:
space:
mode:
authorvincent van beek <vincent@vlogic.nl>2026-03-26 14:02:54 +0100
committerGitHub <noreply@github.com>2026-03-26 13:02:54 +0000
commit0ffde21b0337c606e2d0ece5bd5434a930a87dcd (patch)
tree4fd071728a8da6dcf3e6fc9fe9dca5a492100d34 /opendc-web/opendc-web-server/src/main/webui/util
parent8bb98c773bc982a0dab9cf9fb20d62f60a36a5d7 (diff)
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 <mail.fabianm@gmail.com>
Diffstat (limited to 'opendc-web/opendc-web-server/src/main/webui/util')
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/authorizations.js21
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/available-metrics.js101
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/colors.js29
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/date-time.js81
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/date-time.test.js21
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/effect-ref.js41
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/tile-calculations.js255
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/topology-schema.js47
-rw-r--r--opendc-web/opendc-web-server/src/main/webui/util/unit-specifications.js102
9 files changed, 698 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/authorizations.js b/opendc-web/opendc-web-server/src/main/webui/util/authorizations.js
new file mode 100644
index 00000000..6cb08ba8
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/authorizations.js
@@ -0,0 +1,21 @@
+import HomeIcon from '@patternfly/react-icons/dist/js/icons/home-icon'
+import EditIcon from '@patternfly/react-icons/dist/js/icons/edit-icon'
+import EyeIcon from '@patternfly/react-icons/dist/js/icons/eye-icon'
+
+export const AUTH_ICON_MAP = {
+ OWNER: HomeIcon,
+ EDITOR: EditIcon,
+ VIEWER: EyeIcon,
+}
+
+export const AUTH_NAME_MAP = {
+ OWNER: 'Owner',
+ EDITOR: 'Editor',
+ VIEWER: 'Viewer',
+}
+
+export const AUTH_DESCRIPTION_MAP = {
+ OWNER: 'You own this project',
+ EDITOR: 'You can edit this project',
+ VIEWER: 'You can view this project',
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/available-metrics.js b/opendc-web/opendc-web-server/src/main/webui/util/available-metrics.js
new file mode 100644
index 00000000..fda6cd4d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/available-metrics.js
@@ -0,0 +1,101 @@
+export const METRIC_GROUPS = {
+ 'Host Metrics': [
+ 'total_overcommitted_burst',
+ 'total_power_draw',
+ 'total_failure_vm_slices',
+ 'total_granted_burst',
+ 'total_interfered_burst',
+ 'total_requested_burst',
+ 'mean_cpu_usage',
+ 'mean_cpu_demand',
+ 'mean_num_deployed_images',
+ 'max_num_deployed_images',
+ ],
+ 'Compute Service Metrics': ['total_vms_submitted', 'total_vms_queued', 'total_vms_finished', 'total_vms_failed'],
+}
+
+export const AVAILABLE_METRICS = [
+ 'mean_cpu_usage',
+ 'mean_cpu_demand',
+ 'total_requested_burst',
+ 'total_granted_burst',
+ 'total_overcommitted_burst',
+ 'total_interfered_burst',
+ 'total_power_draw',
+ 'total_failure_vm_slices',
+ 'mean_num_deployed_images',
+ 'max_num_deployed_images',
+ 'total_vms_submitted',
+ 'total_vms_queued',
+ 'total_vms_finished',
+ 'total_vms_failed',
+]
+
+export const METRIC_NAMES_SHORT = {
+ total_overcommitted_burst: 'Overcomm. CPU Cycles',
+ total_granted_burst: 'Granted CPU Cycles',
+ total_requested_burst: 'Requested CPU Cycles',
+ total_interfered_burst: 'Interfered CPU Cycles',
+ total_power_draw: 'Total Power Consumption',
+ mean_cpu_usage: 'Mean Host CPU Usage',
+ mean_cpu_demand: 'Mean Host CPU Demand',
+ mean_num_deployed_images: 'Mean Num. Deployed Images Per Host',
+ max_num_deployed_images: 'Max. Num. Deployed Images Per Host',
+ total_failure_vm_slices: 'Total Num. Failed VM Slices',
+ total_vms_submitted: 'VMs Submitted',
+ total_vms_queued: 'VMs Queued',
+ total_vms_finished: 'VMs Finished',
+ total_vms_failed: 'VMs Failed',
+}
+
+export const METRIC_NAMES = {
+ total_overcommitted_burst: 'Overcommitted CPU Cycles',
+ total_granted_burst: 'Granted CPU Cycles',
+ total_requested_burst: 'Requested CPU Cycles',
+ total_interfered_burst: 'Interfered CPU Cycles',
+ total_power_draw: 'Total Power Consumption',
+ mean_cpu_usage: 'Mean Host CPU Usage',
+ mean_cpu_demand: 'Mean Host CPU Demand',
+ mean_num_deployed_images: 'Mean Number of Deployed Images Per Host',
+ max_num_deployed_images: 'Maximum Number Deployed Images Per Host',
+ total_failure_vm_slices: 'Failed VM Slices',
+ total_vms_submitted: 'VMs Submitted',
+ total_vms_queued: 'VMs Queued',
+ total_vms_finished: 'VMs Finished',
+ total_vms_failed: 'VMs Failed',
+}
+
+export const METRIC_UNITS = {
+ total_overcommitted_burst: 'MFLOP',
+ total_granted_burst: 'MFLOP',
+ total_requested_burst: 'MFLOP',
+ total_interfered_burst: 'MFLOP',
+ total_power_draw: 'Wh',
+ mean_cpu_usage: 'MHz',
+ mean_cpu_demand: 'MHz',
+ mean_num_deployed_images: 'VMs',
+ max_num_deployed_images: 'VMs',
+ total_failure_vm_slices: 'VM Slices',
+ total_vms_submitted: 'VMs',
+ total_vms_queued: 'VMs',
+ total_vms_finished: 'VMs',
+ total_vms_failed: 'VMs',
+}
+
+export const METRIC_DESCRIPTIONS = {
+ total_overcommitted_burst:
+ 'The total CPU clock cycles lost due to overcommitting of resources. This metric is an indicator for resource overload.',
+ total_requested_burst: 'The total CPU clock cycles that were requested by all virtual machines.',
+ total_granted_burst: 'The total CPU clock cycles executed by the hosts.',
+ total_interfered_burst: 'The total CPU clock cycles lost due to resource interference between virtual machines.',
+ total_power_draw: 'The average power usage in watts.',
+ mean_cpu_usage: 'The average amount of CPU clock cycles consumed by all virtual machines on a host.',
+ mean_cpu_demand: 'The average amount of CPU clock cycles requested by all powered on virtual machines on a host.',
+ mean_num_deployed_images: 'The average number of virtual machines deployed on a host.',
+ max_num_deployed_images: 'The maximum number of virtual machines deployed at any time.',
+ total_failure_vm_slices: 'The total amount of CPU clock cycles lost due to failure.',
+ total_vms_submitted: 'The number of virtual machines scheduled by the compute service.',
+ total_vms_queued: 'The number of virtual machines still waiting to be scheduled by the compute service.',
+ total_vms_finished: 'The number of virtual machines that completed.',
+ total_vms_failed: 'The number of virtual machines that could not be scheduled.',
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/colors.js b/opendc-web/opendc-web-server/src/main/webui/util/colors.js
new file mode 100644
index 00000000..34468503
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/colors.js
@@ -0,0 +1,29 @@
+export const GRID_COLOR = 'rgba(0, 0, 0, 0.5)'
+export const BACKDROP_COLOR = 'rgba(255, 255, 255, 1)'
+export const WALL_COLOR = 'rgba(0, 0, 0, 1)'
+
+export const ROOM_DEFAULT_COLOR = 'rgba(150, 150, 150, 1)'
+export const ROOM_IN_CONSTRUCTION_COLOR = 'rgba(51, 153, 255, 1)'
+export const ROOM_HOVER_VALID_COLOR = 'rgba(51, 153, 255, 1)'
+export const ROOM_HOVER_INVALID_COLOR = 'rgba(255, 102, 0, 1)'
+export const ROOM_NAME_COLOR = 'rgba(245, 245, 245, 1)'
+export const ROOM_TYPE_COLOR = 'rgba(245, 245, 245, 1)'
+
+export const TILE_PLUS_COLOR = 'rgba(0, 0, 0, 1)'
+
+export const OBJECT_BORDER_COLOR = 'rgba(0, 0, 0, 1)'
+
+export const RACK_BACKGROUND_COLOR = 'rgba(170, 170, 170, 1)'
+export const RACK_SPACE_BAR_BACKGROUND_COLOR = 'rgba(222, 235, 247, 0.6)'
+export const RACK_SPACE_BAR_FILL_COLOR = 'rgba(91, 155, 213, 0.7)'
+export const RACK_ENERGY_BAR_BACKGROUND_COLOR = 'rgba(255, 242, 204, 0.6)'
+export const RACK_ENERGY_BAR_FILL_COLOR = 'rgba(244, 215, 0, 0.7)'
+export const COOLING_ITEM_BACKGROUND_COLOR = 'rgba(40, 50, 230, 1)'
+export const PSU_BACKGROUND_COLOR = 'rgba(230, 50, 60, 1)'
+
+export const GRAYED_OUT_AREA_COLOR = 'rgba(0, 0, 0, 0.6)'
+
+export const SIM_LOW_COLOR = 'rgba(197, 224, 180, 1)'
+export const SIM_MID_LOW_COLOR = 'rgba(255, 230, 153, 1)'
+export const SIM_MID_HIGH_COLOR = 'rgba(248, 203, 173, 1)'
+export const SIM_HIGH_COLOR = 'rgba(249, 165, 165, 1)'
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/date-time.js b/opendc-web/opendc-web-server/src/main/webui/util/date-time.js
new file mode 100644
index 00000000..7e2f6623
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/date-time.js
@@ -0,0 +1,81 @@
+/**
+ * Parses and formats the given date-time string representation.
+ *
+ * The format assumed is "YYYY-MM-DDTHH:MM:SS".
+ *
+ * @param dateTimeString A string expressing a date and a time, in the above mentioned format.
+ * @returns {string} A human-friendly string version of that date and time.
+ */
+export function parseAndFormatDateTime(dateTimeString) {
+ return formatDateTime(new Date(dateTimeString))
+}
+
+/**
+ * Serializes the given date and time value to a human-friendly string.
+ *
+ * @param dateTime An object representation of a date and time.
+ * @returns {string} A human-friendly string version of that date and time.
+ */
+export function formatDateTime(dateTime) {
+ let date
+ const currentDate = new Date()
+
+ date =
+ addPaddingToTwo(dateTime.getDay()) +
+ '/' +
+ addPaddingToTwo(dateTime.getMonth()) +
+ '/' +
+ addPaddingToTwo(dateTime.getFullYear())
+
+ if (dateTime.getFullYear() === currentDate.getFullYear() && dateTime.getMonth() === currentDate.getMonth()) {
+ if (dateTime.getDate() === currentDate.getDate()) {
+ date = 'Today'
+ } else if (dateTime.getDate() === currentDate.getDate() - 1) {
+ date = 'Yesterday'
+ }
+ }
+
+ return date + ', ' + addPaddingToTwo(dateTime.getHours()) + ':' + addPaddingToTwo(dateTime.getMinutes())
+}
+
+/**
+ * Formats the given number of seconds/ticks to a formatted time representation.
+ *
+ * @param seconds The number of seconds.
+ * @returns {string} A string representation of that amount of second, in the from of HH:MM:SS.
+ */
+export function convertSecondsToFormattedTime(seconds) {
+ if (seconds <= 0) {
+ return '0s'
+ }
+
+ let hour = Math.floor(seconds / 3600)
+ let minute = Math.floor(seconds / 60) % 60
+ let second = seconds % 60
+
+ hour = isNaN(hour) ? 0 : hour
+ minute = isNaN(minute) ? 0 : minute
+ second = isNaN(second) ? 0 : second
+
+ if (hour === 0 && minute === 0) {
+ return second + 's'
+ } else if (hour === 0) {
+ return minute + 'm' + addPaddingToTwo(second) + 's'
+ } else {
+ return hour + 'h' + addPaddingToTwo(minute) + 'm' + addPaddingToTwo(second) + 's'
+ }
+}
+
+/**
+ * Pads the given integer to have at least two digits.
+ *
+ * @param integer An integer to be padded.
+ * @returns {string} A string containing the padded integer.
+ */
+function addPaddingToTwo(integer) {
+ if (integer < 10) {
+ return '0' + integer.toString()
+ } else {
+ return integer.toString()
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/date-time.test.js b/opendc-web/opendc-web-server/src/main/webui/util/date-time.test.js
new file mode 100644
index 00000000..431e39f7
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/date-time.test.js
@@ -0,0 +1,21 @@
+import { convertSecondsToFormattedTime } from './date-time'
+
+describe('tick formatting', () => {
+ it("returns '0s' for numbers <= 0", () => {
+ expect(convertSecondsToFormattedTime(-1)).toEqual('0s')
+ expect(convertSecondsToFormattedTime(0)).toEqual('0s')
+ })
+ it('returns only seconds for values under a minute', () => {
+ expect(convertSecondsToFormattedTime(1)).toEqual('1s')
+ expect(convertSecondsToFormattedTime(59)).toEqual('59s')
+ })
+ it('returns seconds and minutes for values under an hour', () => {
+ expect(convertSecondsToFormattedTime(60)).toEqual('1m00s')
+ expect(convertSecondsToFormattedTime(61)).toEqual('1m01s')
+ expect(convertSecondsToFormattedTime(3599)).toEqual('59m59s')
+ })
+ it('returns full time for values over an hour', () => {
+ expect(convertSecondsToFormattedTime(3600)).toEqual('1h00m00s')
+ expect(convertSecondsToFormattedTime(3601)).toEqual('1h00m01s')
+ })
+})
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/effect-ref.js b/opendc-web/opendc-web-server/src/main/webui/util/effect-ref.js
new file mode 100644
index 00000000..78528585
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/effect-ref.js
@@ -0,0 +1,41 @@
+/*
+ * 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 { useCallback, useRef } from 'react'
+
+const noop = () => {}
+
+/**
+ * A hook that will invoke the specified callback when the reference returned by this function is initialized.
+ * The callback can return an optional clean-up function.
+ */
+export function useEffectRef(callback, deps = []) {
+ const disposeRef = useRef(noop)
+ return useCallback((element) => {
+ disposeRef.current()
+ disposeRef.current = noop
+
+ if (element) {
+ disposeRef.current = callback(element) || noop
+ }
+ }, deps) // eslint-disable-line react-hooks/exhaustive-deps
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/tile-calculations.js b/opendc-web/opendc-web-server/src/main/webui/util/tile-calculations.js
new file mode 100644
index 00000000..374ca48c
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/tile-calculations.js
@@ -0,0 +1,255 @@
+export function deriveWallLocations(tiles) {
+ const { verticalWalls, horizontalWalls } = getWallSegments(tiles)
+ return mergeWallSegments(verticalWalls, horizontalWalls)
+}
+
+function getWallSegments(tiles) {
+ const verticalWalls = {}
+ const horizontalWalls = {}
+
+ tiles.forEach((tile) => {
+ const x = tile.positionX,
+ y = tile.positionY
+
+ for (let dX = -1; dX <= 1; dX++) {
+ for (let dY = -1; dY <= 1; dY++) {
+ if (Math.abs(dX) === Math.abs(dY)) {
+ continue
+ }
+
+ let doInsert = true
+ for (const tile of tiles) {
+ if (tile.positionX === x + dX && tile.positionY === y + dY) {
+ doInsert = false
+ break
+ }
+ }
+ if (!doInsert) {
+ continue
+ }
+
+ if (dX === -1) {
+ if (verticalWalls[x] === undefined) {
+ verticalWalls[x] = []
+ }
+ if (verticalWalls[x].indexOf(y) === -1) {
+ verticalWalls[x].push(y)
+ }
+ } else if (dX === 1) {
+ if (verticalWalls[x + 1] === undefined) {
+ verticalWalls[x + 1] = []
+ }
+ if (verticalWalls[x + 1].indexOf(y) === -1) {
+ verticalWalls[x + 1].push(y)
+ }
+ } else if (dY === -1) {
+ if (horizontalWalls[y] === undefined) {
+ horizontalWalls[y] = []
+ }
+ if (horizontalWalls[y].indexOf(x) === -1) {
+ horizontalWalls[y].push(x)
+ }
+ } else if (dY === 1) {
+ if (horizontalWalls[y + 1] === undefined) {
+ horizontalWalls[y + 1] = []
+ }
+ if (horizontalWalls[y + 1].indexOf(x) === -1) {
+ horizontalWalls[y + 1].push(x)
+ }
+ }
+ }
+ }
+ })
+
+ return { verticalWalls, horizontalWalls }
+}
+
+function mergeWallSegments(vertical, horizontal) {
+ const result = []
+ const walls = [vertical, horizontal]
+
+ for (let i = 0; i < 2; i++) {
+ const wallList = walls[i]
+ for (let a in wallList) {
+ a = parseInt(a, 10)
+
+ wallList[a].sort((a, b) => {
+ return a - b
+ })
+
+ let startPos = wallList[a][0]
+ const isHorizontal = i === 1
+
+ if (wallList[a].length === 1) {
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: 1,
+ })
+ } else {
+ let consecutiveCount = 1
+ for (let b = 0; b < wallList[a].length - 1; b++) {
+ if (b + 1 === wallList[a].length - 1) {
+ if (wallList[a][b + 1] - wallList[a][b] > 1) {
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: consecutiveCount,
+ })
+ consecutiveCount = 0
+ startPos = wallList[a][b + 1]
+ }
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: consecutiveCount + 1,
+ })
+ break
+ } else if (wallList[a][b + 1] - wallList[a][b] > 1) {
+ const startPosX = isHorizontal ? startPos : a
+ const startPosY = isHorizontal ? a : startPos
+ result.push({
+ startPosX,
+ startPosY,
+ isHorizontal,
+ length: consecutiveCount,
+ })
+ startPos = wallList[a][b + 1]
+ consecutiveCount = 0
+ }
+ consecutiveCount++
+ }
+ }
+ }
+ }
+
+ return result
+}
+
+export function deriveValidNextTilePositions(rooms, selectedTiles) {
+ const result = [],
+ newPosition = { x: 0, y: 0 }
+ let isSurroundingTile
+
+ selectedTiles.forEach((tile) => {
+ const x = tile.positionX
+ const y = tile.positionY
+ result.push({ x, y })
+
+ for (let dX = -1; dX <= 1; dX++) {
+ for (let dY = -1; dY <= 1; dY++) {
+ if (Math.abs(dX) === Math.abs(dY)) {
+ continue
+ }
+
+ newPosition.x = x + dX
+ newPosition.y = y + dY
+
+ isSurroundingTile = true
+ for (let index in selectedTiles) {
+ if (
+ selectedTiles[index].positionX === newPosition.x &&
+ selectedTiles[index].positionY === newPosition.y
+ ) {
+ isSurroundingTile = false
+ break
+ }
+ }
+
+ if (isSurroundingTile && findPositionInRooms(rooms, newPosition.x, newPosition.y) === -1) {
+ result.push({ x: newPosition.x, y: newPosition.y })
+ }
+ }
+ }
+ })
+
+ return result
+}
+
+export function findPositionInPositions(positions, positionX, positionY) {
+ for (let i = 0; i < positions.length; i++) {
+ const position = positions[i]
+ if (positionX === position.x && positionY === position.y) {
+ return i
+ }
+ }
+
+ return -1
+}
+
+export function findPositionInRooms(rooms, positionX, positionY) {
+ for (let i = 0; i < rooms.length; i++) {
+ const room = rooms[i]
+ if (findPositionInTiles(room.tiles, positionX, positionY) !== -1) {
+ return i
+ }
+ }
+
+ return -1
+}
+
+function findPositionInTiles(tiles, positionX, positionY) {
+ let index = -1
+
+ for (let i = 0; i < tiles.length; i++) {
+ const tile = tiles[i]
+ if (positionX === tile.positionX && positionY === tile.positionY) {
+ index = i
+ break
+ }
+ }
+
+ return index
+}
+
+export function findTileWithPosition(tiles, positionX, positionY) {
+ for (let i = 0; i < tiles.length; i++) {
+ if (tiles[i].positionX === positionX && tiles[i].positionY === positionY) {
+ return tiles[i]
+ }
+ }
+
+ return null
+}
+
+export function calculateRoomListBounds(rooms) {
+ const min = { x: Number.MAX_VALUE, y: Number.MAX_VALUE }
+ const max = { x: -1, y: -1 }
+
+ rooms.forEach((room) => {
+ room.tiles.forEach((tile) => {
+ if (tile.positionX < min.x) {
+ min.x = tile.positionX
+ }
+ if (tile.positionY < min.y) {
+ min.y = tile.positionY
+ }
+
+ if (tile.positionX > max.x) {
+ max.x = tile.positionX
+ }
+ if (tile.positionY > max.y) {
+ max.y = tile.positionY
+ }
+ })
+ })
+
+ max.x++
+ max.y++
+
+ const center = {
+ x: min.x + (max.x - min.x) / 2.0,
+ y: min.y + (max.y - min.y) / 2.0,
+ }
+
+ return { min, center, max }
+}
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/topology-schema.js b/opendc-web/opendc-web-server/src/main/webui/util/topology-schema.js
new file mode 100644
index 00000000..ff672dd6
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/topology-schema.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 { schema } from 'normalizr'
+
+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(
+ 'machines',
+ {
+ cpus: [Cpu],
+ gpus: [Gpu],
+ memories: [Memory],
+ storages: [Storage],
+ },
+ { idAttribute: 'id' }
+)
+
+export const Rack = new schema.Entity('racks', { machines: [Machine] }, { idAttribute: 'id' })
+
+export const Tile = new schema.Entity('tiles', { rack: Rack }, { idAttribute: 'id' })
+
+export const Room = new schema.Entity('rooms', { tiles: [Tile] }, { idAttribute: 'id' })
+
+export const Topology = new schema.Entity('topologies', { rooms: [Room] }, { idAttribute: 'id' })
diff --git a/opendc-web/opendc-web-server/src/main/webui/util/unit-specifications.js b/opendc-web/opendc-web-server/src/main/webui/util/unit-specifications.js
new file mode 100644
index 00000000..3e3671cd
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/webui/util/unit-specifications.js
@@ -0,0 +1,102 @@
+export const CPU_UNITS = {
+ 'cpu-1': {
+ id: 'cpu-1',
+ name: 'Intel i7 v6 6700k',
+ clockRateMhz: 4100,
+ numberOfCores: 4,
+ energyConsumptionW: 70,
+ },
+ 'cpu-2': {
+ id: 'cpu-2',
+ name: 'Intel i5 v6 6700k',
+ clockRateMhz: 3500,
+ numberOfCores: 2,
+ energyConsumptionW: 50,
+ },
+ 'cpu-3': {
+ id: 'cpu-3',
+ name: 'Intel® Xeon® E-2224G',
+ clockRateMhz: 3500,
+ numberOfCores: 4,
+ energyConsumptionW: 71,
+ },
+ 'cpu-4': {
+ id: 'cpu-4',
+ name: 'Intel® Xeon® E-2244G',
+ clockRateMhz: 3800,
+ numberOfCores: 8,
+ energyConsumptionW: 71,
+ },
+ 'cpu-5': {
+ id: 'cpu-5',
+ name: 'Intel® Xeon® E-2246G',
+ clockRateMhz: 3600,
+ numberOfCores: 12,
+ energyConsumptionW: 80,
+ },
+}
+
+export const GPU_UNITS = {
+ 'gpu-1': {
+ id: 'gpu-1',
+ name: 'NVIDIA GTX 4 1080',
+ clockRateMhz: 1200,
+ numberOfCores: 200,
+ energyConsumptionW: 250,
+ },
+ 'gpu-2': {
+ id: 'gpu-2',
+ name: 'NVIDIA Tesla V100',
+ clockRateMhz: 1200,
+ numberOfCores: 5120,
+ energyConsumptionW: 250,
+ },
+}
+
+export const MEMORY_UNITS = {
+ 'memory-1': {
+ id: 'memory-1',
+ name: 'Samsung PC DRAM K4A4G045WD',
+ speedMbPerS: 16000,
+ sizeMb: 4000,
+ energyConsumptionW: 10,
+ },
+ 'memory-2': {
+ id: 'memory-2',
+ name: 'Samsung PC DRAM M393A2K43BB1-CRC',
+ speedMbPerS: 2400,
+ sizeMb: 16000,
+ energyConsumptionW: 10,
+ },
+ 'memory-3': {
+ id: 'memory-3',
+ name: 'Crucial MTA18ASF4G72PDZ-3G2E1',
+ speedMbPerS: 3200,
+ sizeMb: 32000,
+ energyConsumptionW: 10,
+ },
+ 'memory-4': {
+ id: 'memory-4',
+ name: 'Crucial MTA9ASF2G72PZ-3G2E1',
+ speedMbPerS: 3200,
+ sizeMb: 16000,
+ energyConsumptionW: 10,
+ },
+}
+
+export const STORAGE_UNITS = {
+ 'storage-1': {
+ id: 'storage-1',
+ name: 'Samsung EVO 2016 SATA III',
+ speedMbPerS: 6000,
+ sizeMb: 250000,
+ energyConsumptionW: 10,
+ },
+ 'storage-2': {
+ id: 'storage-2',
+ name: 'Western Digital MTA9ASF2G72PZ-3G2E1',
+ speedMbPerS: 6000,
+ sizeMb: 4000000,
+ energyConsumptionW: 10,
+ },
+}