From a9da76621c1be7a11bf292e868a8f7c22f2ea203 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sun, 26 Mar 2023 21:20:42 +0100 Subject: 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 --- .../web/server/rest/user/TopologyResource.java | 10 ++ .../web/server/rest/user/TopologyResourceTest.java | 16 ++++ opendc-web/opendc-web-ui/src/api/index.js | 2 +- .../src/components/projects/TopologyTable.js | 104 +++++++++++++-------- opendc-web/opendc-web-ui/src/data/topology.js | 4 +- 5 files changed, 93 insertions(+), 43 deletions(-) (limited to 'opendc-web') 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 ( - - - - Name - Rooms - Last Edited - - - - {topologies.map((topology) => ( - - - {topology.name} - - - {topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`} - - {parseAndFormatDateTime(topology.updatedAt)} - - - - - ))} - {topologies.length === 0 && ( + <> + + {error && ( + setError(null)} + /> + } + /> + )} + + + - - - - - + Name + Rooms + Last Edited - )} - - + + + {topologies.map((topology) => ( + + + + {topology.name} + + + + {topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`} + + {parseAndFormatDateTime(topology.updatedAt)} + + + + + ))} + {topologies.length === 0 && ( + + + + + + + + )} + + + ) } 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) } -- cgit v1.2.3