diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2023-03-26 21:20:42 +0100 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2023-03-26 21:22:18 +0100 |
| commit | a9da76621c1be7a11bf292e868a8f7c22f2ea203 (patch) | |
| tree | 51aa097044559a45d18b9dbe44002a0dd2af2bf1 /opendc-web | |
| parent | 6bc9b999ff0d9a0ad2c1c5bea1554abc30a06c5b (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
Diffstat (limited to 'opendc-web')
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) } |
