summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui/src
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2023-03-30 22:58:30 +0100
committerGitHub <noreply@github.com>2023-03-30 22:58:30 +0100
commit0db9d47d2b3062ca867e0a7aa33ba7205307d062 (patch)
tree3d6fc9128dd9ff82434c8ad112a01d023791cba5 /opendc-web/opendc-web-ui/src
parent526d6cd6b48b30cf7bbe40478d57bbc67e7027cc (diff)
parente7d5c086832a24f3c6b98258b0b8eb1fbbd3336a (diff)
merge: Address issues with web UI (#145)
This pull request addresses several issues that have been reported for the OpenDC web UI. ## Implementation Notes :hammer_and_pick: * Update dependencies for web UI * Inform user when deleted topology is still used * Do not offset hover layer after dragging * Fix access to machines on lower shelves * Do not allow selection of empty unit * Fix rack deletion Fixes #135, #136, #137, #138, #139
Diffstat (limited to 'opendc-web/opendc-web-ui/src')
-rw-r--r--opendc-web/opendc-web-ui/src/api/index.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js104
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/map/MapStage.js12
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/map/layers/HoverLayerComponent.js10
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/map/layers/ObjectHoverLayer.js4
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/map/layers/RoomHoverLayer.js4
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/MachineSidebar.js8
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js2
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListComponent.js91
-rw-r--r--opendc-web/opendc-web-ui/src/data/topology.js4
-rw-r--r--opendc-web/opendc-web-ui/src/redux/actions/topology/room.js2
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/topology/index.js2
-rw-r--r--opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js11
-rw-r--r--opendc-web/opendc-web-ui/src/util/effect-ref.js6
14 files changed, 145 insertions, 117 deletions
diff --git a/opendc-web/opendc-web-ui/src/api/index.js b/opendc-web/opendc-web-ui/src/api/index.js
index 75751658..3411b96e 100644
--- a/opendc-web/opendc-web-ui/src/api/index.js
+++ b/opendc-web/opendc-web-ui/src/api/index.js
@@ -49,7 +49,7 @@ export async function request(auth, path, method = 'GET', body) {
const json = await response.json()
if (!response.ok) {
- throw response.message
+ throw json.message
}
return json
diff --git a/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
index 62deace0..1c2c4f04 100644
--- a/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
+++ b/opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
@@ -20,18 +20,22 @@
* SOFTWARE.
*/
-import { Bullseye } from '@patternfly/react-core'
+import { Bullseye, AlertGroup, Alert, AlertVariant, AlertActionCloseButton } from '@patternfly/react-core'
import PropTypes from 'prop-types'
import Link from 'next/link'
import { Tr, Th, Thead, Td, ActionsColumn, Tbody, TableComposable } from '@patternfly/react-table'
-import React from 'react'
+import React, { useState } from 'react'
import TableEmptyState from '../util/TableEmptyState'
import { parseAndFormatDateTime } from '../../util/date-time'
import { useTopologies, useDeleteTopology } from '../../data/topology'
function TopologyTable({ projectId }) {
+ const [error, setError] = useState('')
+
const { status, data: topologies = [] } = useTopologies(projectId)
- const { mutate: deleteTopology } = useDeleteTopology()
+ const { mutate: deleteTopology } = useDeleteTopology({
+ onError: (error) => setError(error),
+ })
const actions = ({ number }) => [
{
@@ -42,45 +46,65 @@ function TopologyTable({ projectId }) {
]
return (
- <TableComposable aria-label="Topology List" variant="compact">
- <Thead>
- <Tr>
- <Th>Name</Th>
- <Th>Rooms</Th>
- <Th>Last Edited</Th>
- </Tr>
- </Thead>
- <Tbody>
- {topologies.map((topology) => (
- <Tr key={topology.id}>
- <Td dataLabel="Name">
- <Link href={`/projects/${projectId}/topologies/${topology.number}`}>{topology.name}</Link>
- </Td>
- <Td dataLabel="Rooms">
- {topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`}
- </Td>
- <Td dataLabel="Last Edited">{parseAndFormatDateTime(topology.updatedAt)}</Td>
- <Td isActionCell>
- <ActionsColumn items={actions(topology)} />
- </Td>
- </Tr>
- ))}
- {topologies.length === 0 && (
+ <>
+ <AlertGroup isToast>
+ {error && (
+ <Alert
+ isLiveRegion
+ variant={AlertVariant.danger}
+ title={error}
+ actionClose={
+ <AlertActionCloseButton
+ title={error}
+ variantLabel="danger alert"
+ onClose={() => setError(null)}
+ />
+ }
+ />
+ )}
+ </AlertGroup>
+ <TableComposable aria-label="Topology List" variant="compact">
+ <Thead>
<Tr>
- <Td colSpan={3}>
- <Bullseye>
- <TableEmptyState
- status={status}
- loadingTitle="Loading topologies"
- emptyTitle="No topologies"
- emptyText="You have not created any topology for this project yet. Click the New Topology button to create one."
- />
- </Bullseye>
- </Td>
+ <Th>Name</Th>
+ <Th>Rooms</Th>
+ <Th>Last Edited</Th>
</Tr>
- )}
- </Tbody>
- </TableComposable>
+ </Thead>
+ <Tbody>
+ {topologies.map((topology) => (
+ <Tr key={topology.id}>
+ <Td dataLabel="Name">
+ <Link href={`/projects/${projectId}/topologies/${topology.number}`}>
+ {topology.name}
+ </Link>
+ </Td>
+ <Td dataLabel="Rooms">
+ {topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`}
+ </Td>
+ <Td dataLabel="Last Edited">{parseAndFormatDateTime(topology.updatedAt)}</Td>
+ <Td isActionCell>
+ <ActionsColumn items={actions(topology)} />
+ </Td>
+ </Tr>
+ ))}
+ {topologies.length === 0 && (
+ <Tr>
+ <Td colSpan={3}>
+ <Bullseye>
+ <TableEmptyState
+ status={status}
+ loadingTitle="Loading topologies"
+ emptyTitle="No topologies"
+ emptyText="You have not created any topology for this project yet. Click the New Topology button to create one."
+ />
+ </Bullseye>
+ </Td>
+ </Tr>
+ )}
+ </Tbody>
+ </TableComposable>
+ </>
)
}
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 1b9fff72..e2b626ec 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,9 +1,8 @@
-import React, { useRef, useState, useContext } from 'react'
+import React, { useRef, useState } from 'react'
import PropTypes from 'prop-types'
import { useHotkeys } from 'react-hotkeys-hook'
import { Stage } from 'react-konva'
import { MAP_MAX_SCALE, MAP_MIN_SCALE, MAP_MOVE_PIXELS_PER_EVENT, MAP_SCALE_PER_EVENT } from './MapConstants'
-import { ReactReduxContext } from 'react-redux'
import useResizeObserver from 'use-resize-observer'
import { mapContainer } from './MapStage.module.css'
import MapLayer from './layers/MapLayer'
@@ -13,7 +12,6 @@ import ScaleIndicator from './controls/ScaleIndicator'
import Toolbar from './controls/Toolbar'
function MapStage({ hotkeysRef }) {
- const reduxContext = useContext(ReactReduxContext)
const stageRef = useRef(null)
const { width = 500, height = 500 } = useResizeObserver({ ref: stageRef.current?.attrs?.container })
const [[x, y], setPos] = useState([0, 0])
@@ -68,11 +66,9 @@ function MapStage({ hotkeysRef }) {
x={x}
y={y}
>
- <ReactReduxContext.Provider value={reduxContext}>
- <MapLayer />
- <RoomHoverLayer />
- <ObjectHoverLayer />
- </ReactReduxContext.Provider>
+ <MapLayer />
+ <RoomHoverLayer />
+ <ObjectHoverLayer />
</Stage>
<ScaleIndicator scale={scale} />
<Toolbar onZoom={onZoomButton} onExport={onExport} />
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/HoverLayerComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/HoverLayerComponent.js
index 2b1060c0..d7e0c56a 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/map/layers/HoverLayerComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/map/layers/HoverLayerComponent.js
@@ -15,11 +15,11 @@ function HoverLayerComponent({ isEnabled, isValid, onClick, children }) {
const stage = layer.getStage()
- // Transform used to convert mouse coordinates to world coordinates
- const transform = stage.getAbsoluteTransform().copy()
- transform.invert()
-
stage.on('mousemove.hover', () => {
+ // Transform used to convert mouse coordinates to world coordinates
+ const transform = stage.getAbsoluteTransform().copy()
+ transform.invert()
+
const { x, y } = transform.point(stage.getPointerPosition())
setPos([x, y])
})
@@ -38,7 +38,7 @@ function HoverLayerComponent({ isEnabled, isValid, onClick, children }) {
const y = gridY * TILE_SIZE_IN_PIXELS
return (
- <Layer opacity={0.6} ref={layerRef}>
+ <Layer opacity={0.2} ref={layerRef}>
<HoverTile x={x} y={y} isValid={valid} onClick={() => (valid ? onClick(gridX, gridY) : undefined)} />
{children ? React.cloneElement(children, { x, y, scale: 1 }) : undefined}
</Layer>
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 1f00de36..5e741a3b 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
@@ -27,7 +27,7 @@ import { findTileWithPosition } from '../../../../util/tile-calculations'
import HoverLayerComponent from './HoverLayerComponent'
import TilePlusIcon from '../elements/TilePlusIcon'
-function ObjectHoverLayer() {
+export default function ObjectHoverLayer() {
const isEnabled = useSelector((state) => state.construction.inRackConstructionMode)
const isValid = useSelector((state) => (x, y) => {
if (state.interactionLevel.mode !== 'ROOM') {
@@ -49,5 +49,3 @@ function ObjectHoverLayer() {
</HoverLayerComponent>
)
}
-
-export default ObjectHoverLayer
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 727f4e25..b9cfcaf4 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
@@ -30,7 +30,7 @@ import {
} from '../../../../util/tile-calculations'
import HoverLayerComponent from './HoverLayerComponent'
-function RoomHoverLayer() {
+export default function RoomHoverLayer() {
const dispatch = useDispatch()
const onClick = (x, y) => dispatch(toggleTileAtLocation(x, y))
const isEnabled = useSelector((state) => state.construction.currentRoomInConstruction !== '-1')
@@ -57,5 +57,3 @@ function RoomHoverLayer() {
return <HoverLayerComponent onClick={onClick} isEnabled={isEnabled} isValid={isValid} />
}
-
-export default RoomHoverLayer
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 6f89e10b..8a4c33dc 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
@@ -15,7 +15,13 @@ import { useSelector } from 'react-redux'
function MachineSidebar({ tileId, position }) {
const machine = useSelector(({ topology }) => {
const rack = topology.racks[topology.tiles[tileId].rack]
- return topology.machines[rack.machines[position - 1]]
+
+ for (const machineId of rack.machines) {
+ const machine = topology.machines[machineId]
+ if (machine.position === position) {
+ return machine
+ }
+ }
})
const machineId = machine.id
return (
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js
index 4507b409..18cba23a 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddComponent.js
@@ -27,7 +27,7 @@ function UnitAddComponent({ units, onAdd }) {
</SelectOption>
))}
</Select>
- <Button icon={<PlusIcon />} variant="control" onClick={() => onAdd(selected)}>
+ <Button icon={<PlusIcon />} variant="control" onClick={() => onAdd(selected)} isDisabled={!selected}>
Add
</Button>
</InputGroup>
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListComponent.js
index de7a2140..02c97730 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineListComponent.js
@@ -17,49 +17,56 @@ import { Machine } from '../../../../shapes'
function MachineListComponent({ machines = [], onSelect, onAdd }) {
return (
<DataList aria-label="Rack Units">
- {machines.map((machine, index) =>
- machine ? (
- <DataListItem key={index} onClick={() => onSelect(index + 1)}>
- <DataListItemRow>
- <DataListItemCells
- dataListCells={[
- <DataListCell isIcon key="icon">
- <Badge isRead>{machines.length - index}U</Badge>
- </DataListCell>,
- <DataListCell key="primary content">
- <MachineComponent onClick={() => onSelect(index + 1)} machine={machine} />
- </DataListCell>,
- ]}
- />
- <DataListAction id="goto" aria-label="Goto Machine" aria-labelledby="goto">
- <Button isSmall variant="plain" className="pf-u-p-0">
- <AngleRightIcon />
- </Button>
- </DataListAction>
- </DataListItemRow>
- </DataListItem>
- ) : (
- <DataListItem key={index}>
- <DataListItemRow>
- <DataListItemCells
- dataListCells={[
- <DataListCell isIcon key="icon">
- <Badge isRead>{machines.length - index}U</Badge>
- </DataListCell>,
- <DataListCell key="add" className="text-secondary">
- Empty Slot
- </DataListCell>,
- ]}
- />
- <DataListAction id="add" aria-label="Add Machine" aria-labelledby="add">
- <Button isSmall variant="plain" className="pf-u-p-0" onClick={() => onAdd(index + 1)}>
- <PlusIcon />
- </Button>
- </DataListAction>
- </DataListItemRow>
- </DataListItem>
+ {machines
+ .map((machine, index) =>
+ machine ? (
+ <DataListItem key={index} onClick={() => onSelect(index + 1)}>
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell isIcon key="icon">
+ <Badge isRead>{index + 1}U</Badge>
+ </DataListCell>,
+ <DataListCell key="primary content">
+ <MachineComponent onClick={() => onSelect(index + 1)} machine={machine} />
+ </DataListCell>,
+ ]}
+ />
+ <DataListAction id="goto" aria-label="Goto Machine" aria-labelledby="goto">
+ <Button isSmall variant="plain" className="pf-u-p-0">
+ <AngleRightIcon />
+ </Button>
+ </DataListAction>
+ </DataListItemRow>
+ </DataListItem>
+ ) : (
+ <DataListItem key={index}>
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell isIcon key="icon">
+ <Badge isRead>{index + 1}U</Badge>
+ </DataListCell>,
+ <DataListCell key="add" className="text-secondary">
+ Empty Slot
+ </DataListCell>,
+ ]}
+ />
+ <DataListAction id="add" aria-label="Add Machine" aria-labelledby="add">
+ <Button
+ isSmall
+ variant="plain"
+ className="pf-u-p-0"
+ onClick={() => onAdd(index + 1)}
+ >
+ <PlusIcon />
+ </Button>
+ </DataListAction>
+ </DataListItemRow>
+ </DataListItem>
+ )
)
- )}
+ .reverse()}
</DataList>
)
}
diff --git a/opendc-web/opendc-web-ui/src/data/topology.js b/opendc-web/opendc-web-ui/src/data/topology.js
index ac6cabe5..d5e624d5 100644
--- a/opendc-web/opendc-web-ui/src/data/topology.js
+++ b/opendc-web/opendc-web-ui/src/data/topology.js
@@ -83,6 +83,6 @@ export function useNewTopology() {
/**
* Create a mutation for deleting a topology.
*/
-export function useDeleteTopology() {
- return useMutation('deleteTopology')
+export function useDeleteTopology(options = {}) {
+ return useMutation('deleteTopology', options)
}
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 350c1d92..14cc126c 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
@@ -53,10 +53,10 @@ export function addRackToTile(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/topology/index.js b/opendc-web/opendc-web-ui/src/redux/reducers/topology/index.js
index b1c7d29e..2c849387 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/index.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/index.js
@@ -35,7 +35,7 @@ function objects(state = {}, action) {
storages: STORAGE_UNITS,
machines: machine(state.machines, action, state),
racks: rack(state.racks, action, state),
- tiles: tile(state.tiles, action, state),
+ tiles: tile(state.tiles, action),
rooms: room(state.rooms, action, state),
root: topology(state.root, action, state),
}
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
index 8e5ecd6e..24c0e20c 100644
--- a/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js
+++ b/opendc-web/opendc-web-ui/src/redux/reducers/topology/tile.js
@@ -26,7 +26,7 @@ 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 }) {
+function tile(state = {}, action) {
switch (action.type) {
case STORE_TOPOLOGY:
return action.entities.tiles || {}
@@ -42,14 +42,13 @@ function tile(state = {}, action, { racks }) {
})
case ADD_RACK_TO_TILE:
return produce(state, (draft) => {
- const { rack } = action
- draft[rack.tileId].rack = rack.id
+ const { rack, tileId } = action
+ draft[tileId].rack = rack.id
})
case DELETE_RACK:
return produce(state, (draft) => {
- const { rackId } = action
- const rack = racks[rackId]
- draft[rack.tileId].rack = undefined
+ const { tileId } = action
+ draft[tileId].rack = undefined
})
default:
return state
diff --git a/opendc-web/opendc-web-ui/src/util/effect-ref.js b/opendc-web/opendc-web-ui/src/util/effect-ref.js
index cda0324b..78528585 100644
--- a/opendc-web/opendc-web-ui/src/util/effect-ref.js
+++ b/opendc-web/opendc-web-ui/src/util/effect-ref.js
@@ -26,9 +26,9 @@ 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.
+ * The callback can return an optional clean-up function.
*/
-export function useEffectRef(callback) {
+export function useEffectRef(callback, deps = []) {
const disposeRef = useRef(noop)
return useCallback((element) => {
disposeRef.current()
@@ -37,5 +37,5 @@ export function useEffectRef(callback) {
if (element) {
disposeRef.current = callback(element) || noop
}
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
+ }, deps) // eslint-disable-line react-hooks/exhaustive-deps
}