summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2023-03-26 21:20:42 +0100
committerFabian Mastenbroek <mail.fabianm@gmail.com>2023-03-26 21:22:18 +0100
commita9da76621c1be7a11bf292e868a8f7c22f2ea203 (patch)
tree51aa097044559a45d18b9dbe44002a0dd2af2bf1
parent6bc9b999ff0d9a0ad2c1c5bea1554abc30a06c5b (diff)
bug(web): Inform user when deleted topology is still used
This change fixes #135 which showed that trying to delete a topology used by a scenario would result in nothing happening in the UI and a 500 error being returned by the server. We check whether a scenario still references the topology and show an error to the user if that happens. Fixes #135
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java10
-rw-r--r--opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java16
-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/data/topology.js4
5 files changed, 93 insertions, 43 deletions
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 2b66b64b..71491801 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
@@ -22,10 +22,12 @@
package org.opendc.web.server.rest.user;
+import io.quarkus.hibernate.orm.panache.Panache;
import io.quarkus.security.identity.SecurityIdentity;
import java.time.Instant;
import java.util.List;
import javax.annotation.security.RolesAllowed;
+import javax.persistence.PersistenceException;
import javax.transaction.Transactional;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
@@ -193,6 +195,14 @@ public final class TopologyResource {
entity.updatedAt = Instant.now();
entity.delete();
+
+ try {
+ // Flush the results, so we can check whether the constraints are not violated
+ Panache.flush();
+ } catch (PersistenceException e) {
+ throw new WebApplicationException("Topology is still in use", 403);
+ }
+
return UserProtocol.toDto(entity, auth);
}
}
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 21e35b09..c0746e7a 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
@@ -355,4 +355,20 @@ public final class TopologyResourceTest {
.statusCode(200)
.contentType(ContentType.JSON);
}
+
+ /**
+ * Test to delete a topology that is still being used by a scenario.
+ */
+ @Test
+ @TestSecurity(
+ user = "owner",
+ roles = {"openid"})
+ public void testDeleteUsed() {
+ given().pathParam("project", "1")
+ .when()
+ .delete("/1") // Topology 1 is still used by scenario 1 and 2
+ .then()
+ .statusCode(403)
+ .contentType(ContentType.JSON);
+ }
}
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/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)
}